{"owner":"generalaction","repo":"emdash","hasSkills":true,"totalSkillsCount":39,"totalTokensCount":49115,"categories":["root-instruction","claude-rule","subagent-persona","command-prompt","plugin-manifest","anthropic-skill"],"hasMcp":false,"mcpConfig":null,"found":["AGENTS.md","CLAUDE.md","agents/README.md","agents/architecture/path-system.md","apps/emdash-desktop/src/core/primitives/commands/README.md","packages/core/README.md","packages/core/docs/README.md","packages/core/docs/path/README.md","packages/core/docs/path/boundaries.md","packages/core/docs/path/normalization.md","packages/core/docs/path/recipes.md","packages/core/docs/path/resources.md","packages/core/src/primitives/sqlite-store/README.md","packages/core/src/runtimes/lsp/README.md","packages/shared/README.md","packages/wire/docs/README.md","packages/wire/docs/api/clients.md","packages/wire/docs/api/contracts.md","packages/wire/docs/api/errors.md","packages/wire/docs/api/files.md","packages/wire/docs/api/middleware.md","packages/wire/docs/api/serving.md","packages/wire/docs/api/transports.md","packages/wire/docs/live/event-stream.md","packages/wire/docs/live/live-job.md","packages/wire/docs/live/live-log.md","packages/wire/docs/live/live-model.md","packages/wire/docs/live/mutations.md","packages/wire/docs/live/replicas.md","packages/wire/docs/observability.md","packages/wire/docs/runtime/components.md","packages/wire/docs/runtime/lifecycle.md","packages/wire/docs/runtime/mailbox-and-broadcast.md","packages/wire/docs/runtime/resource-cache.md","packages/wire/docs/runtime/scheduling.md","packages/wire/docs/runtime/structured-concurrency.md","packages/wire/docs/runtime/workers.md","packages/wire/examples/README.md","packages/wire/src/state/README.md"],"skills":{"AGENTS.md":"# Project Overview\n\nEmdash is a local-first, cross-platform Electron app for running multiple AI coding\nagents in parallel. Each task is isolated in its own Git worktree and can run locally\nor over SSH, while the desktop app coordinates provider CLIs, ACP chat sessions,\nterminal sessions, issue and PR integrations, diff review, and release packaging.\n\n## Repository Structure\n\nThis is a pnpm workspace monorepo. The Electron app lives in\n`apps/emdash-desktop/` as `@emdash/emdash-desktop`; shared packages live under\n`packages/`. Unless a path is prefixed with `packages/` or another app, app paths\nsuch as `src/...`, `drizzle/`, `scripts/`, and `build/` are relative to\n`apps/emdash-desktop/`.\n\nRepo root:\n\n- `.claude/` - Local Claude agent settings for this checkout.\n- `.github/` - GitHub issue templates, reusable actions, CI, and release workflows.\n- `agents/` - Agent-facing architecture, workflow, convention, integration, and risk docs.\n- `apps/emdash-desktop/` - The Electron desktop app.\n- `apps/workspace-server/` - Remote workspace server and its Docker-based dev stack.\n- `packages/chat-ui/` - Shared transcript and ACP chat renderer with Storybook coverage.\n- `packages/core/` - Transport-agnostic runtime primitives, including ACP session logic.\n- `packages/plugins/` - Agent provider plugin definitions, hooks, and ACP adapters.\n- `packages/shared/` - Shared primitives such as result types, logging, and markdown helpers.\n- `packages/theme/` - Theme token codegen that emits the generated theme CSS.\n- `packages/ui/` - Shared React UI components, theme tokens, recipes, and primitives.\n- `packages/wire/` - Typed wire protocol contracts shared by app and workspace server.\n- `pnpm-workspace.yaml` - Workspace package globs for `apps/*` and `packages/**`.\n- Root config files - `package.json`, `nx.json`, `.nvmrc`, `.oxfmtrc.json`,\n  `.oxlintrc.json`, and lockfile/configuration owned at the workspace root.\n\nInside `apps/emdash-desktop/`:\n\n- `build/` - Electron packaging assets; avoid edits unless working on packaging/signing.\n- `drizzle/` - Generated Drizzle SQL migrations and metadata.\n- `scripts/` - Release, verification, and build support scripts.\n- `src/main/` - Electron main process: bootstrap, Wire gateway, DB, and host services.\n- `src/entry/` - Electron entry points: `main.ts` (main process) and `preload.ts`, the typed\n  preload bridge exposed to the renderer.\n- `src/renderer/` - React composition shell, shared browser infrastructure, and tests.\n- `src/core/` - Vertical slices with portable APIs, Node implementations, browser UI, and manifests.\n- `src/types/` - Ambient and cross-cutting TypeScript declarations.\n- `tooling/` - App-level development and test infrastructure not bundled into production.\n\n## Build & Development Commands\n\nThe only prerequisite is any `pnpm` on PATH: the root `package.json` pins\n`packageManager: pnpm@10.28.2` and `devEngines.runtime` node `24.14.0` with\n`onFail: \"download\"`, so pnpm provisions the pinned toolchain itself (no nvm or\ncorepack). The committed `mise.toml` is an optional convenience for mise users.\nRoot scripts are powered by Nx and run package targets in dependency order with\nlocal caching where configured.\n\nGolden-path commands (see [Developer flows](agents/workflows/flows.md) for the\nfull index — one blessed command per flow, database flows, Storybook,\npackaging, remote development, and escape hatches):\n\n```bash\npnpm install        # complete setup from the repo root — nothing else needed\npnpm run doctor     # report-only environment health check\npnpm run dev        # full workspace dev (root); app-only from apps/emdash-desktop/\npnpm run build      # build all workspace projects\npnpm run check      # full merge gate: format, lint, typecheck, test\n```\n\nThe four gate commands also run individually from the repo root:\n\n```bash\npnpm run format\npnpm run lint\npnpm run typecheck\npnpm run test\n```\n\nUse an isolated development database for schema or migration work:\n\n```bash\nEMDASH_DB_FILE=/tmp/emdash-scratch.db pnpm run dev\n```\n\nRun focused database validation from `apps/emdash-desktop/`:\n\n```bash\npnpm run db:fixtures\npnpm run test:migrations\n```\n\nDeploy releases only when explicitly asked to do release work:\n\n```bash\ngh workflow run release-prod.yml --ref main -f arch=both\ngh workflow run release-canary.yml --ref main -f arch=both\ngh workflow run release-workspace-server.yml --ref main -f channel=stable\n```\n\nProduction releases publish artifacts to GitHub Releases and Cloudflare R2. Canary\nreleases currently publish to R2 only. See\n[workspace-server packaging](apps/workspace-server/docs/packaging.md) for its independent release\nand channel rules.\n\n## Code Style & Conventions\n\n- Use `pnpm`; do not introduce npm or yarn lockfile churn.\n- Format with `oxfmt`; config is `.oxfmtrc.json`.\n- Keep formatted lines near the configured `printWidth` of 100 characters.\n- Use 2 spaces, semicolons, single quotes in TS, double quotes in JSX, LF endings,\n  trailing commas where valid in ES5, and sorted imports.\n- Lint with `oxlint`; config is `.oxlintrc.json` with correctness, TypeScript, React\n  hooks, and local repo rules enabled.\n- TypeScript strict mode is enabled; app targets share `apps/emdash-desktop/tsconfig.json`.\n- Avoid `any`; if a registry or boundary needs it, keep the escape local and documented.\n- Use top-level `import` statements; do not use `require()`.\n- Never re-export as a shortcut; import from the original source.\n- Components use `PascalCase`; hooks use `useX` camelCase or an existing local pattern.\n- Tests use `*.test.ts` or `*.test.tsx`.\n- Slice Wire controllers live in `src/core/features/<feature>/node/` and are aggregated by\n  `src/core/manifests/node/controllers.ts`; they delegate to imported operation or service\n  functions.\n- Renderer-main calls go through Wire via the seeded connection seam in\n  `src/core/primitives/wire/browser/connection.ts`: the renderer bootstrap seeds it once at\n  startup, and each slice exposes a typed domain client from its `api/` (for example\n  `getMcpClient()`) built on `domainClient`.\n- Feature UI lives under `src/core/features/<feature>/browser/`; the editor slice owns Monaco and\n  file rendering under `src/core/features/editor/browser/`; portable browser primitives (modals,\n  views, navigation, commands, scoped stores) live under `src/core/primitives/`; the thin\n  renderer host shell lives under `src/renderer/lib/`.\n- New feature modals and views must be exposed through the owning slice's `contributions/browser.ts`\n  and aggregated by `src/core/manifests/browser/browser-contributions.ts`.\n- New task tabs contribute providers through `src/core/features/<feature>/contributions/tabs.ts`,\n  aggregated by `src/core/manifests/browser/task-tab-contributions.ts`; the workbench registry\n  lives at `src/core/features/workbench/api/browser/task-tab-registry.ts`.\n- New commands are defined with `defineCommand` (`src/core/primitives/commands/`) in the owning\n  slice's `contributions/commands.ts`, aggregated by\n  `src/core/manifests/shared/command-catalog.ts`; view scopes\n  (`src/core/primitives/view-scopes/`) bind availability and implementation.\n- Commit messages should follow Conventional Commits:\n\n```text\n<type>(<scope>): <short imperative summary>\n\nExamples:\nfix(opencode): change initialPromptFlag from -p to --prompt for TUI\nfeat(docs): add changelog tab with GitHub releases integration\n```\n\n## Architecture Notes\n\n```mermaid\nflowchart LR\n  User[User] --> Renderer[React renderer]\n  Renderer --> Clients[Typed Wire domain clients]\n  Clients --> Seam[Seeded wire connection seam]\n  Seam --> Preload[Preload wire port]\n  Preload --> Main[Electron main process]\n  Main --> Gateway[Desktop Wire gateway]\n  Gateway --> Controllers[Slice Wire controllers]\n  Controllers --> Services[Domain services]\n  Services --> DB[(SQLite via Drizzle)]\n  Services --> Runtime[Runtime services]\n  Runtime --> PTY[PTY sessions]\n  Runtime --> ACP[ACP sessions]\n  Runtime --> SSH[SSH and SFTP]\n  Services --> VCS[Git, GitHub, GitLab, PRs]\n  Services --> Issues[Issue integrations]\n  Services --> MCP[MCP and skills]\n  ACP --> CoreAcp[@emdash/core ACP runtime]\n  ACP --> Plugins[@emdash/plugins providers]\n  Renderer --> ChatUI[@emdash/chat-ui]\n  Main --> Events[Live models and typed events]\n  Events --> Renderer\n```\n\nThe app boots from `src/entry/main.ts`, which runs the phased bootstrap in\n`src/main/bootstrap/` (environment, database, window creation, recovery). Renderer-main\ntraffic is served by the desktop Wire gateway (`src/main/gateway/desktop-wire.ts`) from the\ncontract assembled in `src/core/manifests/shared/desktop-wire-contract.ts` and the controllers\naggregated in `src/core/manifests/node/controllers.ts`. The preload bridge\n(`src/entry/preload.ts`) exposes only `requestWirePort` and `getPathForFile`. The renderer is\na React app that calls typed Wire domain clients, subscribes to live models and typed events,\nand coordinates views, tabs, modals, commands, project state, terminal state, and task\nworkflows.\n\nTask execution has two runtime paths. Legacy/TUI conversations run through PTY\nservices under `packages/core/src/services/pty/` and the terminal/TUI runtimes under\n`packages/core/src/runtimes/terminals/` and `packages/core/src/runtimes/tui-agents/`.\nStructured chat conversations use ACP: provider plugins in `packages/plugins/` expose\nACP behavior, `packages/core/src/runtimes/acp/` owns protocol/session state, terminal\nmanagement, and process hosting, the desktop launches that runtime as a Wire component worker\nfrom `src/main/gateway/entries/acp.ts` (`src/main/core/acp/` bridges agent status), and\n`src/core/features/conversations/browser/acp/` maps updates into `@emdash/chat-ui`.\n\nMain-process adapter domains live under `src/main/core/`: ACP, agent status, app,\ndependencies, file search, files, Git, preview servers, provider accounts, runtime,\nshared, terminal shell, and utils. Portable domain logic lives in vertical slices\nunder `src/core/` and in `packages/core/` (for example PTY services under\n`packages/core/src/services/pty/` and resource monitoring under\n`packages/core/src/runtimes/resource-usage/`). Expected failures should use the\n`Result<T, E>` pattern from `@emdash/shared` or the app-local result helpers.\n\n## Testing Strategy\n\nLocal merge gate:\n\n```bash\npnpm run format\npnpm run lint\npnpm run typecheck\npnpm run test\n```\n\n- Root `pnpm run test` uses Nx to run every workspace package test target.\n- App tests run with Vitest projects in `apps/emdash-desktop/vitest.config.ts`.\n- App `node` tests cover `src/**/*.test.ts` except DB, migration, and browser tests.\n- App `main-db` tests cover main-process integration tests that need real SQLite.\n- App `fixtures` tests generate DB fixtures via `pnpm run db:fixtures`.\n- App `migrations` tests validate Drizzle migrations via `pnpm run test:migrations`.\n- App `scripts` tests cover release and support scripts under `scripts/**/*.test.ts`.\n- App `browser` tests use Playwright-backed `@vitest/browser` for renderer behavior.\n- `packages/core` has ACP, dependency, plugin helper, Git, FS, and runtime unit tests.\n- `packages/chat-ui` has node, browser, perf, and benchmark test targets.\n- `packages/ui`, `packages/shared`, and `packages/plugins` run their package-local tests.\n- Integration-style tests create temporary repos and worktrees in `os.tmpdir()`.\n- CI runs `.github/workflows/code-consistency-check.yml` with `nx affected` for\n  `format:check`, `typecheck`, `lint`, and `test` on touched projects and\n  dependents; the Playwright-backed `browser` Vitest projects are skipped there.\n- Tests are still expected locally before merge even where CI coverage is narrower.\n\n## Security & Compliance\n\n- The project is licensed under Apache-2.0; see `LICENSE.md`.\n- Do not commit secrets, tokens, private keys, app databases, logs, build artifacts,\n  generated dependency folders, or release artifacts.\n- Application secrets are stored through encrypted app secret services and Electron\n  safe storage; SSH credentials are managed through SSH services and OS-backed storage.\n- Release secrets live in GitHub Actions secrets, including PostHog, Cloudflare R2,\n  Apple signing/notarization, Azure Trusted Signing, and Cachix credentials.\n- Telemetry must remain optional; users can disable it with `TELEMETRY_ENABLED=false`\n  or in app settings.\n- File logging must preserve redaction of common secret patterns.\n- PTY environment construction must go through `packages/core/src/services/pty/api/terminal-env.ts`.\n- Treat ACP process spawning, SSH command construction, shell escaping, PTY spawning,\n  and worktree paths as security-sensitive.\n- Do not bypass path-safety, shell escaping, or validation helpers.\n- Use `pnpm-lock.yaml` for dependency integrity and review dependency changes.\n- This checkout does not define repo-local Dependabot config, CODEOWNERS, or SECURITY.md;\n  do not assume extra repository-owned security automation beyond the workflows present\n  in `.github/workflows/`.\n- Dependency changes must keep `pnpm-lock.yaml` in sync, preserve `packageManager` and\n  `pnpm.onlyBuiltDependencies`, and avoid introducing new install scripts or native\n  builds without explicit review.\n- CI installs with `pnpm install --frozen-lockfile --ignore-scripts` in\n  `.github/workflows/code-consistency-check.yml`; changes that rely on install-time\n  side effects need clear justification.\n- Prefer existing dependencies and workspace packages over adding new third-party\n  packages. When adding a dependency, document why the existing stack is insufficient\n  and check license/security posture before committing the lockfile change.\n\n## Agent Guardrails\n\n- Start with this file for repo-wide context and required commands.\n- Load only the relevant `agents/` topic page for the area you are changing.\n- Prefer updating the smallest applicable `agents/` page over expanding this file.\n- If nested `AGENTS.md` files are added later, the closest file to the edited path wins.\n- Explicit user or maintainer instructions override this file.\n- Do not hand-edit numbered Drizzle migrations or `drizzle/meta/`.\n- Use `pnpm run db:generate` for new migrations, then update fixtures and migration tests.\n- Avoid editing `dist/`, `release/`, `out/`, `build/`, and generated package artifacts\n  unless the task is explicitly about packaging, signing, or release behavior.\n- Do not dispatch release workflows, publish packages, upload artifacts, or trigger\n  external deployments unless the user explicitly asks for release work.\n- Treat `src/main/core/acp/`, `packages/core/src/services/pty/`,\n  `src/core/services/ssh/`, `src/main/db/`, updater code, and provider process\n  spawning as high risk.\n- Read the matching `agents/risky-areas/` page before touching database, PTY, SSH, or\n  updater code.\n- Do not weaken shell quoting, spawn behavior, env allowlists, path validation, or\n  secret redaction casually.\n- Prefer existing service, provider, plugin, Wire, modal, view, tab, and store patterns\n  over new abstractions.\n- New Wire procedures belong in the owning slice's `api/` contract with a controller in\n  `node/`, registered through `src/core/manifests/shared/desktop-wire-contract.ts` and\n  `src/core/manifests/node/controllers.ts`.\n- Keep renderer-main calls on typed Wire contracts, live models, and typed events. The preload\n  bridge should stay small; add direct `window.electronAPI` surface only when an\n  Electron/browser primitive cannot fit the Wire path.\n- Access task and Project MobX stores through selectors and task view hooks:\n  `getTaskStore`, `asProvisioned`, `taskViewKind`, `getTaskManagerStore`,\n  `getProjectStore`, `asAvailableProject`, `getProjectHostAccess`, `useTaskViewKind`, `useWorkspace`,\n  `useWorkspaceId`, `useDevServers`, `useWorkspaceViewModel`, `useConversations`,\n  and `useTerminals`.\n- Never use `asProvisioned(...)!` or `asAvailableProject(...)!`; use explicit null checks.\n- State guards must check `kind !== 'ready'` rather than enumerating non-ready states.\n- Access task managers through `getTaskManagerStore(projectId)`, not `project.taskManager`.\n- Access available Project contexts through `asAvailableProject(getProjectStore(id))`, not inline\n  guards. Read Host-dependent action state through that context's `host` access interface.\n- Task selectors live in `src/core/features/tasks/api/browser/task-state/task-selectors.ts`.\n- Project selectors live in `src/core/features/projects/api/browser/stores/project-selectors.ts`.\n- For provider changes, update plugin metadata, shared provider metadata, ACP support\n  flags, PTY env passthrough if needed, hook integrations, renderer assumptions, and\n  tests for non-standard behavior.\n- For ACP changes, preserve protocol state-machine behavior in `packages/core/src/runtimes/acp/`,\n  keep provider-specific transforms in `packages/plugins/`, and adapt UI payloads at\n  app or chat-UI edges.\n- For MCP changes, keep canonical data in shared types and adapt provider formats at edges.\n- Follow `.github/PULL_REQUEST_TEMPLATE.md`: keep PRs small and focused, self-review\n  before handoff, list checks run, attach UI evidence when applicable, and update docs\n  and tests when behavior changes.\n- Call out high-risk changes explicitly in the PR description or handoff notes,\n  especially database, ACP, PTY, SSH, updater, provider spawning, dependency, and\n  release-related changes.\n- Do not self-approve, merge, assign reviewers, edit branch protection, or change\n  workflow permissions unless the user explicitly asks.\n- Avoid scripted loops against GitHub, Linear, Jira, GitLab, provider CLIs, or release\n  workflows. Use focused queries and respect existing workflow retries; ask before\n  adding polling, bulk API calls, or scheduled automation.\n- Keep automation scoped to the task. Do not run the full local merge gate repeatedly\n  when a focused check is enough during iteration; run broader checks before handoff\n  when the change scope justifies it.\n\n## Extensibility Hooks\n\n- Agent provider plugins live in `packages/plugins/src/agents/impl/` and are registered\n  in `packages/plugins/src/agents/registry.ts`.\n- Provider plugin hosting services live in `packages/core/src/services/agent-plugins/`.\n- Agent provider metadata and capabilities live in `packages/plugins/src/agents/registry.ts`\n  and `packages/plugins/src/agents/impl/`; renderer-facing DTOs are built by\n  `src/core/features/agents/node/agent-payload-builder.ts`.\n- ACP support is exposed through plugin ACP capabilities and portable Core ACP APIs.\n- Provider detection lives in `src/main/core/dependencies/`.\n- Provider PTY behavior and env construction live under `packages/core/src/services/pty/`.\n- Provider event hooks and plugins are installed and hosted by\n `packages/core/src/runtimes/tui-agents/`; desktop projection lives under\n `src/main/core/agent-status/`.\n- ACP process hosting lives in `packages/core/src/runtimes/acp/`; the desktop launches the\n  runtime as a Wire component worker from `src/main/gateway/entries/acp.ts`.\n- Feature modal and view definitions are contributed by slices and aggregated in\n  `src/core/manifests/browser/browser-contributions.ts`; renderer app registries compose host entries.\n- Task tab providers are aggregated through\n  `src/core/manifests/browser/task-tab-contributions.ts`.\n- MCP server config handling lives in `src/core/features/mcp/node/` (wire controller and\n  registration utils), canonical types in `src/core/primitives/mcp/api/`, and UI in\n  `src/core/features/mcp/browser/`.\n- Skills types and validation live in Core primitives; skills UI and service code live in\n  `src/core/features/skills/browser/` and `src/core/features/skills/node/`.\n- Team-owned runtime settings live in each workspace's `.emdash.json`:\n  `preservePatterns`, `scripts.prepare`, `scripts.setup`, `scripts.run`,\n  `scripts.teardown`, and `shellSetup` (a per-workspace override of the host default).\n- Project overrides such as `worktreeRoot`, `defaultBranch`, `baseRemote`, `pushRemote`,\n  `githubAccount`, `agentGitCredentials`, and `tmux` are DB-backed, not `.emdash.json`. Effective\n  tmux resolves through project override > host default > app default; it is not seeded from a\n  default at project creation.\n- The workspace registry owns lifecycle and file-handling config. Host-local personal config\n  (`preservePatterns`, scripts, and auto-run toggles) is versioned JSON on the repository or\n  directory project-root record; worktrees resolve against their project root's personal config.\n- Read registry-owned config through the keyed `projectConfig` live model or `getProjectConfig`.\n  It exposes raw personal/team layers, team-file sources, and one `{ value, from }` result per\n  field. Applicable precedence is personal > that workspace's `.emdash.json` > host default >\n  built-in; arrays replace rather than merge.\n- Per-host defaults (`shellSetup`, `worktreeRoot`, `tmux`) live in the host-settings\n  runtime (`packages/core/src/runtimes/host-settings/`), stored as JSON in the host's\n  emdash data directory and editable from the machines/system settings UI.\n- Desktop project settings compose self-contained domain snapshots: the registry owns\n  lifecycle/file handling, while desktop DB and host-settings domains own Git identity and\n  placement. Forms edit raw layers with explicit per-domain patches; there is no merged settings\n  bag to persist.\n- The scripts runtime is a strict executor: callers must provide resolved `command` and\n  `shellSetup`; it never reads config files or host defaults. The workspace registry resolves and\n  sequences lifecycle commands, then observes runs into durable lifecycle steps.\n- Desktop settings migrations are centralized under\n  `src/core/features/projects/node/settings/migrations/` and run in order when an attachment is\n  established. Legacy schemas and readers stay migration-only; destination markers make imports\n  idempotent and retryable.\n- Optional environment variables include `TELEMETRY_ENABLED`, `EMDASH_DB_FILE`,\n  `EMDASH_DISABLE_NATIVE_DB`, `EMDASH_DISABLE_PTY`, `EMDASH_AUTH_SERVER_URL` (development\n  builds only), `CODEX_SANDBOX_MODE`, and `CODEX_APPROVAL_POLICY`.\n- Build-time telemetry configuration may use `VITE_POSTHOG_KEY` and `VITE_POSTHOG_HOST`.\n- Runtime feature flags are read through telemetry-backed feature flag helpers.\n- App-internal path aliases are defined in `tsconfig.json` (`@/*`, `@core/*`, `@renderer/*`,\n  `@main/*`, `@root/*`, and `@tooling/*`) and mirrored where needed in\n  `electron.vite.config.ts`; workspace packages resolve through their `exports` maps with a\n  `development` condition, not aliases.\n- Versioned JSON column schemas use `defineVersionedSchema()` from\n  `@emdash/core/primitives/versioned-schema/api`\n (`packages/core/src/primitives/versioned-schema/api/versioned-schema.ts`) and Drizzle\n `versionedJsonColumn()` from `src/core/services/app-db/node/versioned-column.ts`.\n\n## Further Reading\n\n- [Agent docs map](agents/README.md)\n- [Quickstart](agents/quickstart.md)\n- [Architecture overview](agents/architecture/overview.md)\n- [Settings ownership and precedence](agents/architecture/settings.md)\n- [Main process architecture](agents/architecture/main-process.md)\n- [Renderer architecture](agents/architecture/renderer.md)\n- [Shared modules](agents/architecture/shared.md)\n- [Developer flows](agents/workflows/flows.md)\n- [Nx task orchestration and caching](agents/workflows/nx.md)\n- [Testing workflow](agents/workflows/testing.md)\n- [Worktrees workflow](agents/workflows/worktrees.md)\n- [Remote development workflow](agents/workflows/remote-development.md)\n- [Workspace server architecture](agents/architecture/workspace-server.md)\n- [Provider integration](agents/integrations/providers.md)\n- [MCP integration](agents/integrations/mcp.md)\n- [IPC conventions](agents/conventions/ipc.md)\n- [Main-process patterns](agents/conventions/main-patterns.md)\n- [Renderer patterns](agents/conventions/renderer-patterns.md)\n- [TypeScript and React conventions](agents/conventions/typescript.md)\n- [Config file rules](agents/conventions/config-files.md)\n- [UI kit conventions](agents/conventions/ui-kit.md)\n- [UI styling conventions](agents/conventions/ui-styling.md)\n- [Versioned schema conventions](agents/conventions/versioned-schemas.md)\n- [Database risk notes](agents/risky-areas/database.md)\n- [PTY risk notes](agents/risky-areas/pty.md)\n- [SSH risk notes](agents/risky-areas/ssh.md)\n- [Updater risk notes](agents/risky-areas/updater.md)\n- [Contributing guide](CONTRIBUTING.md)\n- [Project README](README.md)\n","CLAUDE.md":"@AGENTS.md\n","agents/README.md":"# Agent Docs\n\nThis directory is the system of record for agent-facing repo guidance. Keep topic pages small, specific, and mechanically checkable where possible.\n\n## Recommended Reading Order\n\n1. `quickstart.md`\n2. `architecture/overview.md`\n3. the task-specific page for the area you are changing\n\n## Directory Layout\n\n- `architecture/`\n  - system structure and major code ownership boundaries\n  - [`core-modules.md`](architecture/core-modules.md) — Core runtime, service, and primitive module types, platform surfaces, dependency rules, and subpath exports\n  - [`workspace-server.md`](architecture/workspace-server.md) — protocol versioning, negotiation handshake, and behavior-change rules for the remote workspace daemon\n  - [`state-ownership.md`](architecture/state-ownership.md) — desktop-owned entities, host-owned resources, bindings, and the placement rule for operation behavior\n  - [`acp-runtime.md`](architecture/acp-runtime.md) — ACP runtime, session manager, connection pool, cells, live models, and API contract ownership\n  - [`git-runtime.md`](architecture/git-runtime.md) — Git contract/runtime ownership, session lifecycle, and nested wire composition\n  - [`path-system.md`](architecture/path-system.md) — host-aware path identity, resource URI ownership, and future migration boundaries\n- `workflows/`\n  - task-oriented procedures like testing, worktrees, remote development, and Nx task orchestration\n- `integrations/`\n  - provider, MCP, and external service guidance\n  - [`integration-plugins.md`](integrations/integration-plugins.md) — integration auth and credential contracts, stable account identity, issue-, pull-request-, and repository-plugin contracts, remote-to-integration routing\n- `risky-areas/`\n  - places where incorrect changes are expensive\n- `conventions/`\n  - coding contracts and repo rules\n  - [`ui-kit.md`](conventions/ui-kit.md) — which UI kit to use: `@emdash/ui` for components, feature Tailwind for layout one-offs, where new primitives go, `cn()` location, theming\n  - [`ui-styling.md`](conventions/ui-styling.md) — vanilla-extract styling rules for `packages/ui`\n\n## Maintenance Rules\n\n- Prefer one page per concrete topic.\n- Avoid volatile counts unless you can verify them cheaply.\n- Link to the source-of-truth file paths.\n- Update the smallest relevant page instead of expanding `AGENTS.md`.\n","agents/architecture/path-system.md":"# Path System\n\n`@emdash/core/primitives/path/api` is the source of truth for portable file identity and lexical\npath operations. The detailed package docs live in\n[`packages/core/docs/path/README.md`](../../packages/core/docs/path/README.md).\n\n## Ownership\n\n- `packages/core/src/path/` owns pure structured path primitives, resource URI\n  encoding, and comparison keys.\n- Desktop placement resolution owns repository and worktree location policy. It combines the target\n  host's home directory from `files.getHomeDir`, desktop settings, per-project overrides, and UI\n  input, then sends absolute paths to the host runtime.\n- Repository destinations default to `~/emdash/repositories/<name>` with numeric suffix allocation.\n  Worktree pools default to `~/emdash/worktrees/<repo-basename>-<hash8(repo-path)>`; the workspace\n  planner adds the sanitized branch leaf.\n- Runtime and app layers own host resolution, native string conversion,\n  filesystem I/O, realpath checks, authorization, and persistence migrations.\n- Renderer code should eventually prefer `ResourceUri` or structured refs over\n  raw absolute strings, but existing consumers are not migrated by the foundation\n  module.\n\n## Representation Rules\n\n- Use `HostFileRef` for a full file address on a host.\n- Use `ScopedPath` for root-relative operations such as Git paths, watcher\n  events, tree entries, and bulk calls.\n- Use `ResourceUri` for serialized identity.\n- Use `ResourceKey` for in-memory dedupe and maps.\n- Use explicit `PathProfile` semantics instead of `node:path` behavior when\n  interpreting cross-host paths.\n\n## Future Adoption Map\n\n- Core `files` path helpers can delegate lexical operations to\n  `@emdash/core/primitives/path/api`.\n- Desktop `RuntimePath` and SSH path helpers can become adapters around\n  structured path parsing/formatting.\n- Git models can represent repo-relative file coordinates as\n  `PortableRelativePath`.\n- fs-watch can normalize native events into `ScopedPath` before batching and\n  coalescing.\n- workspace-server schemas can move from documented string conventions to\n  structured path schemas.\n- Renderer tabs, tree nodes, Monaco models, comments, and view-state snapshots\n  can migrate from raw path strings to `ResourceUri`.\n\nDo not add filesystem I/O, `realpath()`, host registry state, or app-specific\nauthorization to `packages/core/src/path/`.\n","apps/emdash-desktop/src/core/primitives/commands/README.md":"# Commands\n\nCommands are portable, schema-backed action definitions. Definitions contain\nstatic metadata and optional keybindings; view scopes provide their runtime\navailability and implementation.\n\n```ts\nconst createBranchCommand = defineCommand({\n  id: 'task.createBranch',\n  title: 'Create Branch',\n  category: 'Git',\n  input: z.object({ branchName: z.string(), baseRef: z.string() }),\n});\n```\n\nCommand input describes arguments supplied for one invocation. Scope params\ndescribe the context in which the command runs; callers must not duplicate\nscope context in command input.\n","packages/core/README.md":"# @emdash/core\n\n`@emdash/core` owns Emdash's shared domain APIs and the host-scoped runtime and\nservice implementations used by the desktop app and workspace-server. It is the\npackage for code that must be shared across hosts without putting that ownership\nin an app package.\n\nCore keeps domain definitions close to their implementations, but separates\nportable APIs from platform-specific code through explicit subpath exports. A\nconsumer should be able to tell from the import path whether it is importing\nshared vocabulary, a Node implementation, or a browser implementation.\n\n## Package Structure\n\nSource modules live under `src/` and are grouped by module type:\n\n```text\nsrc/\n  primitives/\n  services/\n  runtimes/\n  workspace-server/\n```\n\n### Primitives\n\n`primitives/` contains reusable vocabulary and small infrastructure that does\nnot own a product domain. Primitives may define schemas, value objects,\ndeterministic helpers, and narrowly scoped policies.\n\nExamples include host-aware paths, host identity, skill schemas, MCP schemas,\nagent environment helpers, plugin file-system ports, and concurrency helpers.\n\nPrimitives may depend on other primitives, but they must not depend on services\nor runtimes.\n\n### Services\n\n`services/` contains focused capabilities that can be injected into runtimes or\nhost composition code. Services may own bounded resources, expose ports, provide\nNode or browser implementations, or define Wire contracts for smaller shared\ncapabilities.\n\nExamples include execution contexts, PTY management, filesystem watching, host\ndependency detection, and agent plugin support.\n\nServices may depend on primitives and explicitly lower-level services. They must\nnot depend on runtimes.\n\n### Runtimes\n\n`runtimes/` contains host-scoped composition roots for larger product domains.\nA runtime typically implements a public Wire contract, owns domain state and\nresources, and coordinates injected services and primitives.\n\nCurrent runtime domains include:\n\n- `acp`\n- `agent-config`\n- `files`\n- `git`\n- `tui-agents`\n- `workspace`\n\nRuntimes are peers. A runtime must not import another runtime; cross-runtime\ncomposition belongs in the desktop host, workspace-server host, or another\nexplicit composition root.\n\n### Workspace Server\n\n`workspace-server/` is a special composition root, not a fourth general module\ntype. It owns workspace-server protocol versions, shared host schemas, aggregate\nWire contracts, and host-owned composition helpers. It may compose runtime APIs,\nservices, and primitives.\n\n## Surface Conventions\n\nEach module is split by platform surface:\n\n```text\n<module-type>/<module-name>/\n  api/\n  node/\n  browser/\n```\n\nModules only add the surfaces they need.\n\n### `api`\n\n`api/` is the portable public surface. It may contain Wire contracts, serialized\nmodels, DTOs, Zod schemas, public ports, typed errors, and pure helpers needed by\nconsumers.\n\n`api/` must not import `node/` or `browser/`, use platform modules, spawn\nprocesses, or own background work.\n\n### `node`\n\n`node/` contains Node-specific implementations: runtime and service factories,\nWire components, Wire controllers, procedure implementations, filesystem/Git/PTY/subprocess\nbehavior, and resource allocation. App-owned worker entry files adapt components to process hosting\nwith `runWireComponentWorker()`.\n\n### `browser`\n\n`browser/` contains browser-specific implementations of portable APIs. Browser\nsurfaces may import `api/` and other `browser/` surfaces, but must not import\nNode code.\n\n## Subpath Exports\n\nPublic imports must select an explicit module type, module name, and surface:\n\n```text\n@emdash/core/<module-type>/<module-name>/<surface>\n```\n\nExamples:\n\n```ts\nimport { gitContract } from '@emdash/core/runtimes/git/api';\nimport { GitRuntime, gitComponent } from '@emdash/core/runtimes/git/node';\n\nimport type { IWatchService } from '@emdash/core/services/fs-watch/api';\nimport { createNativeWatchService } from '@emdash/core/services/fs-watch/node';\n\nimport { parseAbsolute, type HostAbsolutePath } from '@emdash/core/primitives/path/api';\n```\n\nAvoid ambiguous module-root exports such as `@emdash/core/runtimes/git`.\nConsumers should choose `api`, `node`, or `browser` explicitly.\n\nInside `packages/core/src`, package subpath imports (the `imports` field in\n`package.json`) are used for cross-directory imports:\n\n```ts\nimport { parseAbsolute } from '#primitives/path/api';\nimport { createNativeWatchService } from '#services/fs-watch/node';\nimport { gitContract } from '#runtimes/git/api';\n```\n\nThose subpath imports are internal to Core source and resolve through Core's\nown `package.json`, so consumers need no extra configuration. Code outside\nCore should import through public `@emdash/core/...` subpaths.\n\n## More Details\n\nSee [`../../agents/architecture/core-modules.md`](../../agents/architecture/core-modules.md)\nfor the full architecture notes, dependency rules, and migration guidance.\n","packages/core/docs/README.md":"# @emdash/core Docs\n\n`@emdash/core` contains transport-agnostic primitives shared by the desktop app,\nworkspace server, worker processes, and provider runtimes.\n\n## Public Exports\n\n- [`@emdash/core/primitives/path/api`](path/README.md) - pure host-aware path identity,\n  normalization, resource URI, and comparison helpers.\n\n## Boundaries\n\nCore package docs describe reusable primitives. App-specific wiring, persistence\nmigrations, Electron IPC, and host runtime ownership stay in the app or workspace\nserver packages that own those concerns.\n","packages/core/docs/path/README.md":"# Path System\n\n`@emdash/core/primitives/path/api` defines Emdash's portable file identity vocabulary. It is\npure TypeScript, browser-safe, and does not perform filesystem I/O.\n\nUse this module when a path crosses a process, renderer, persistence, or Wire\nboundary. Runtime adapters remain responsible for converting structured paths\ninto native strings immediately before filesystem, Git, watcher, or process\nspawn operations.\n\n## Concepts\n\n- `HostRef` from `@emdash/core/primitives/host/api` selects a local or remote runtime host.\n- `HostAbsolutePath` is a structured absolute path with an explicit root:\n  POSIX root, Windows drive, or UNC share.\n- `PortableRelativePath` is a normalized `/`-separated coordinate relative to a\n  known root. The empty string represents the root.\n- `HostFileRef` combines `HostRef` and `HostAbsolutePath` into a routeable file\n  address.\n- `ScopedPath` combines a root `HostFileRef` and `PortableRelativePath`.\n- `ResourceUri` is the versioned string form for serialization and persistence.\n- `ResourceKey` is an opaque comparison key for maps and deduplication.\n\n## Basic Flow\n\n```ts\nimport {\n  encodeResourceUri,\n  hostFileRef,\n  parseAbsolute,\n  parsePortableRelativePath,\n  resolveScopedPath,\n  scopedPath,\n} from '@emdash/core/primitives/path/api';\nimport { LOCAL_HOST_REF } from '@emdash/core/primitives/host/api';\n\nconst rootPath = parseAbsolute('/repo', { profile: { style: 'posix' } });\nconst relative = parsePortableRelativePath('src/index.ts');\n\nif (rootPath.success && relative.success) {\n  const root = hostFileRef(LOCAL_HOST_REF, rootPath.data);\n  const scoped = scopedPath(root, relative.data);\n  const resolved = resolveScopedPath(scoped);\n\n  if (resolved.success) {\n    const uri = encodeResourceUri(resolved.data);\n    // emdash-file://v2/local/local/posix/repo/src/index.ts\n  }\n}\n```\n\n`HostFileRef` belongs at routing, persistence, and renderer identity boundaries.\nOnce a domain controller has selected a host-bound Wire connection, Git and Files\nruntime contracts carry `HostAbsolutePath` roots and `PortableRelativePath`\ncoordinates without repeating `HostRef` in every request.\n\n## Related Pages\n\n- [Normalization](normalization.md)\n- [Resources](resources.md)\n- [Boundaries](boundaries.md)\n- [Recipes](recipes.md)\n","packages/core/docs/path/boundaries.md":"# Boundaries\n\nThe path module owns pure identity and lexical path math. It does not own host\nlifecycle, filesystem I/O, authorization, or persistence migrations.\n\n## Conversion Points\n\n```mermaid\nflowchart LR\n  NativeInput[Native string] --> Parser[Path parser]\n  Parser --> Ref[HostFileRef]\n  Ref --> Controller[Domain controller]\n  Controller -->|select by HostRef| Wire[Host-bound Wire connection]\n  Wire --> Runtime[Runtime path key]\n  Runtime --> NativeOutput[Native string]\n  NativeOutput --> Fs[Filesystem or Git]\n```\n\nConvert at these boundaries:\n\n- User input, picker output, SSH responses, and Git roots become structured refs\n  at ingress.\n- Renderer and persistence payloads use `ResourceUri`, `HostFileRef`, or\n  `ScopedPath`, not raw native strings.\n- Domain controllers use `HostRef` to select or relay to a host-bound Wire\n  connection. They do not inject host routing identity into the runtime.\n- Host-bound Wire payloads use `HostAbsolutePath` roots and\n  `PortableRelativePath` coordinates.\n- Host runtimes convert structured paths back to native strings immediately before\n  filesystem, Git, watcher, or process-spawn calls.\n- Watcher and Git events should become `ScopedPath` values before crossing to\n  renderer-facing models.\n\n## Contract Validation\n\nContracts should import schemas from `@emdash/core/path` instead of introducing\nnew `z.string()` path fields once they migrate to structured resources.\n\nRecommended boundary shapes:\n\n- `hostFileRefSchema` for one-off global file resources.\n- `scopedPathSchema` for a single file under a known root.\n- `{ root: hostAbsolutePathSchema, path: portableRelativePathSchema }` for\n  host-bound runtime APIs, tree keys, watcher events, and Git paths.\n- `{ root: hostFileRefSchema, path: portableRelativePathSchema }` while a payload\n  still needs host routing.\n- `resourceUriSchema` for persisted string identity.\n- `absolutePathInputSchema(profile)` for native/user/host input only.\n\nThe schemas delegate to the same parsers as internal code, so contract\nvalidation and local validation enforce the same invariants.\n\n## Lexical Versus Physical Containment\n\n`containsAbsolute()` and `relativizeHostFileRef()` are lexical. They do not\nfollow symlinks.\n\nUse lexical containment for identity, tree projection, deduplication, batching,\nand root-relative path conversion.\n\nUse realpath-based containment for security-sensitive writes, deletes, and\nfilesystem mutation policies. That logic belongs in runtime or app services with\naccess to a filesystem implementation.\n\n## Not Owned Here\n\nThis module intentionally does not implement:\n\n- host connection ownership or mapping from `HostRef` to a live runtime;\n- SSH connection management;\n- workspace mount persistence;\n- `realpath()` or canonical inode checks;\n- renderer tab migrations;\n- Monaco model migration;\n- Wire contract migrations;\n- filesystem authorization.\n\n## Future Adoption Map\n\nExisting consumers can migrate incrementally:\n\n- Files contracts use `HostAbsolutePath` roots and `PortableRelativePath`\n  coordinates; runtime policy adds realpath-based mutation safety.\n- Desktop `RuntimePath` can become a thin native-format adapter around\n  `PathSemantics`.\n- SSH path helpers can use POSIX parsing explicitly instead of app-local string\n  utilities.\n- Git models use repository/worktree `HostAbsolutePath` roots and keep file paths\n  as `PortableRelativePath`.\n- fs-watch can emit root-scoped portable paths, then batch using `ResourceKey`.\n- workspace-server schemas can validate structured refs instead of documenting\n  string conventions only.\n- Renderer tabs, tree nodes, Monaco models, comments, and view-state snapshots\n  can store `ResourceUri` while deriving display paths from workspace mounts.\n","packages/core/docs/path/normalization.md":"# Normalization\n\nPath normalization in Emdash is explicit. Callers choose the dialect they are\nparsing instead of inheriting the operating system of the current process.\n\n## Native Absolute Paths\n\n`parseAbsolute()` accepts a `profile` option.\n\n```ts\nparseAbsolute('/home/david/repo', { profile: { style: 'posix' } });\nparseAbsolute('C:\\\\Users\\\\David\\\\repo', { profile: { style: 'win32' } });\nparseAbsolute('\\\\\\\\server\\\\share\\\\repo', { profile: { style: 'win32' } });\n```\n\nPOSIX parsing treats backslash as a valid filename character. Windows parsing\ntreats both `\\\\` and `/` as separators.\n\nThe parser:\n\n- rejects null bytes;\n- resolves `.` and `..` lexically;\n- rejects paths that escape their root;\n- preserves Unicode spelling by default and normalizes only when requested;\n- preserves path spelling and casing for display;\n- uppercases Windows drive letters in the structured root.\n\nThe parser does not:\n\n- call `realpath()`;\n- check whether the path exists;\n- authorize access;\n- turn a POSIX backslash into a separator.\n\n## Portable Relative Paths\n\n`PortableRelativePath` is used for scoped coordinates such as file tree keys,\nGit paths, watcher events, and root-relative Wire payloads.\n\nRules:\n\n- `/` is the separator.\n- The empty string is the root.\n- Leading `/`, Windows drive roots, and UNC roots are rejected.\n- `.` and `..` are resolved lexically; escaping the root is rejected.\n- Backslash is a literal character, not a separator.\n- Null bytes are rejected.\n\n```ts\nparsePortableRelativePath('src/./components/../index.ts');\n// => 'src/index.ts'\n\nparsePortableRelativePath('../outside');\n// => invalid-path\n```\n\n## Comparison\n\nStored paths preserve spelling. Equality and deduplication use comparison keys\ncreated from an explicit `PathProfile`.\n\n```ts\nconst semantics = createPathSemantics({\n  style: 'win32',\n  caseSensitivity: 'insensitive',\n  unicodeNormalization: 'nfc',\n});\n```\n\nDo not lowercase or otherwise rewrite stored paths to make comparison easier.\nUse `ResourceKey` or `PathSemantics.comparisonKey()` for maps and sets.\n","packages/core/docs/path/recipes.md":"# Recipes\n\n## Parse a Native POSIX Path\n\n```ts\nconst parsed = parseAbsolute('/home/david/repo/src/index.ts', {\n  profile: { style: 'posix' },\n});\n```\n\n## Parse a Native Windows Path\n\n```ts\nconst parsed = parseAbsolute('C:\\\\Users\\\\David\\\\repo\\\\src\\\\index.ts', {\n  profile: { style: 'win32' },\n});\n```\n\n## Build a Host File Ref\n\n```ts\nconst host = hostRef('remote', 'connection-1');\nconst path = parseAbsolute('/repo/src/index.ts', { profile: { style: 'posix' } });\n\nif (path.success) {\n  const ref = hostFileRef(host, path.data);\n}\n```\n\n## Use a Root-Scoped Path\n\n```ts\nconst root = hostFileRef(LOCAL_HOST_REF, rootPath);\nconst relative = parsePortableRelativePath('src/index.ts');\n\nif (relative.success) {\n  const scoped = scopedPath(root, relative.data);\n  const resolved = resolveScopedPath(scoped);\n}\n```\n\nUse this pattern for watcher events, Git paths, file tree nodes, and bulk file\noperations.\n\n## Store a Resource URI\n\n```ts\nconst uri = encodeResourceUri(ref);\nconst decoded = decodeResourceUri(uri);\n```\n\nStore the URI when a durable string identity is needed. Keep `ResourceKey` for\nin-memory maps only.\n\n## Deduplicate with a Resource Key\n\n```ts\nconst key = resourceKeyFromFileRef(ref, {\n  profile: {\n    style: 'win32',\n    caseSensitivity: 'insensitive',\n    unicodeNormalization: 'nfc',\n  },\n});\n\nmodelsByResource.set(key, model);\n```\n\n## Convert an Event Under a Watched Root\n\n```ts\nconst changed = hostFileRef(root.host, changedAbsolutePath);\nconst relative = relativizeHostFileRef(root, changed);\n\nif (relative.success) {\n  const scoped = scopedPath(root, relative.data);\n}\n```\n\nBatch and coalesce watcher events with resource keys derived from the resolved\nfile refs, not by string-prefix comparisons.\n\n## Validate a Contract Model\n\nUse structured schemas for contracts instead of ad hoc `z.string()` path fields.\n\n```ts\nconst readTextInputSchema = z.object({\n  file: scopedPathSchema,\n  options: readFileOptionsSchema.optional(),\n});\n```\n\nFor root-scoped bulk APIs, keeping root and relative path separate is often more\ncompact:\n\n```ts\nconst treePathKeySchema = z.object({\n  root: hostAbsolutePathSchema,\n  path: portableRelativePathSchema,\n  sessionId: z.string(),\n});\n```\n\nThis compact form is appropriate after a domain controller has already selected\nthe host-bound runtime connection. Use `hostFileRefSchema` instead when the same\npayload must still be routed to a host.\n\nFor durable UI state, use a resource URI:\n\n```ts\nconst fileTabStateSchema = z.object({\n  resource: resourceUriSchema,\n});\n```\n\nOnly use native absolute input schemas at ingress boundaries:\n\n```ts\nconst nativePickerInputSchema = z.object({\n  path: absolutePathInputSchema({ profile: { style: 'posix' } }),\n});\n```\n\n## Handle Errors\n\nAll parsing helpers return `Result<T, PathError>`.\n\n```ts\nconst parsed = parsePortableRelativePath(input);\nif (!parsed.success) {\n  switch (parsed.error.type) {\n    case 'invalid-path':\n      return parsed.error.message;\n    case 'outside-root':\n      return 'Path is outside the selected root';\n    default:\n      return 'Invalid resource';\n  }\n}\n```\n","packages/core/docs/path/resources.md":"# Resources\n\n`HostFileRef` is the canonical application-level file identity:\n\n```ts\ntype HostFileRef = {\n  host: HostRef;\n  path: HostAbsolutePath;\n};\n```\n\nIt identifies a logical path on a host. It is not an inode identity. Symlinks,\nhard links, and renames are handled by runtime-specific filesystem behavior.\n\n## Host References\n\n`HostRef` is defined by `@emdash/core/primitives/host/api`:\n\n```ts\ntype HostRef = {\n  type: 'local' | 'remote';\n  id: string;\n};\n```\n\nIt is used by domain controllers to select a host-bound runtime connection. A\nruntime executes on the addressed host and therefore does not need `HostRef` in\nits own Git or Files keys.\n\n## Absolute Roots\n\nStructured roots avoid ambiguity across hosts and drives:\n\n```ts\n{ kind: 'posix' }\n{ kind: 'drive', driveLetter: 'C' }\n{ kind: 'unc', server: 'server', share: 'share' }\n```\n\nThe path segments are stored separately from the root, so `/`, `C:/`, and\n`//server/share` cannot be confused.\n\n## Scoped Paths\n\nUse `ScopedPath` when many operations share a root:\n\n```ts\ntype ScopedPath = {\n  root: HostFileRef;\n  relative: PortableRelativePath;\n};\n```\n\nThis is the preferred globally addressable shape for watcher events, tree\nentries, and batch operations. Host-local runtime contracts use the equivalent\ncompact shape: a `HostAbsolutePath` root plus `PortableRelativePath` coordinates.\n\n## Resource URIs\n\n`ResourceUri` is the stable string encoding:\n\n```text\nemdash-file://v2/local/local/posix/home/david/repo/src/index.ts\nemdash-file://v2/remote/connection-1/drive/c/Users/David/repo/src/index.ts\nemdash-file://v2/remote/connection-2/unc/server/share/repo/src/index.ts\n```\n\nUse it for serialization, persistence, Monaco model identity, and durable\nmessages. Percent encoding is applied per path segment.\n\n## Contract Schemas\n\nPath schemas live next to the path primitives so Wire/RPC contracts and\npersisted models do not duplicate validation rules.\n\nUse:\n\n- `hostFileRefSchema` for a globally addressable file resource.\n- `scopedPathSchema` for a file under a known root.\n- `portableRelativePathSchema` for Git paths, file tree entry keys, and watcher\n  paths that are already scoped by another field.\n- `resourceUriSchema` for persisted or Monaco-style string identity.\n- `resourceRefFromUriSchema` when a boundary accepts a URI string but internal\n  code wants a decoded `HostFileRef`.\n- `absolutePathInputSchema(profile)` only at native/user/host ingress\n  boundaries.\n\nSchemas delegate to the canonical parsers. For example,\n`portableRelativePathSchema` transforms `src/./index.ts` into `src/index.ts`\ninstead of merely branding the original string.\n\n## Resource Keys\n\n`ResourceKey` is an opaque comparison key for maps and deduplication. It may be\ncase-insensitive or Unicode-normalized depending on the selected profile.\n\nDo not persist `ResourceKey` as the only representation of a file. It is lossy by\ndesign and may not preserve display spelling.\n","packages/core/src/primitives/sqlite-store/README.md":"# SQLite stores\n\n`sqlite-store` standardizes how Emdash opens, migrates, tests, backs up, and\nrebuilds SQLite files. It deliberately does not depend on an ORM or a particular\nSQLite package. A host supplies a `SqliteDriver` and may attach an ORM through\n`createOrm`.\n\n## Choose a store type\n\n| Concern | Durable store | Derived store |\n| --- | --- | --- |\n| Intended data | User-authored or otherwise irreplaceable | Cache or index reproducible from another source |\n| Schema changes | Ordered migration history | Drop and rebuild |\n| Version state | `__emdash_migrations` plus runner metadata | Schema fingerprint in `PRAGMA user_version` |\n| Backups | Optional, before pending migrations | Not supported |\n| Downgrade behavior | Unknown newer migration rows are tolerated | Any version mismatch rebuilds |\n| Migration tests | `openAtMigration()` | Rebuild/idempotence tests |\n\nUse `defineDurableSqliteStore()` unless losing the whole database is always safe.\nA derived store's `createSchema` must be capable of recreating everything from\nscratch at any time.\n\n## Defining stores\n\n### Durable\n\nThe application owns the driver adapter and ORM attachment:\n\n```ts\nimport { defineDurableSqliteStore } from '@emdash/core/primitives/sqlite-store/node';\nimport { assertSqliteStoreInvariants } from '@emdash/core/primitives/sqlite-store/testing';\nimport { drizzle } from 'drizzle-orm/better-sqlite3';\nimport { betterSqlite3Driver } from './better-sqlite3-driver';\nimport { migrations } from './migrations.generated';\nimport * as schema from './schema';\n\nexport const appStore = defineDurableSqliteStore({\n  name: 'app',\n  driver: betterSqlite3Driver,\n  migrations,\n  createOrm: (connection) => drizzle(connection.native, { schema }),\n  backup: { retain: 2 },\n  invariants: [assertSqliteStoreInvariants],\n});\n\nconst handle = appStore.open('/path/to/emdash.db');\nhandle.db.select().from(schema.projects);\nhandle.close();\n```\n\n`createOrm` is optional. Without it, `handle.db` is the `SqliteConnection`.\n`postMigrate` hooks run with foreign keys enabled whenever the latest schema is\nopened. `invariants` run after those hooks when migrations were applied, for\nevery `openTemp()`, and after every `migrateToLatest()`. No-op production opens\nskip invariants so full database scans do not become a startup cost.\n\n### Derived\n\n```ts\nimport {\n  defineDerivedSqliteStore,\n  fingerprintDerivedSchema,\n  betterSqlite3Driver,\n} from '@emdash/core/primitives/sqlite-store/node';\n\nconst schemaSql = `\n  CREATE TABLE indexed_paths (\n    id INTEGER PRIMARY KEY,\n    path TEXT NOT NULL\n  ) STRICT\n`;\n\nexport const searchStore = defineDerivedSqliteStore({\n  name: 'file-search',\n  driver: betterSqlite3Driver,\n  version: fingerprintDerivedSchema(schemaSql),\n  createSchema(connection) {\n    connection.exec(schemaSql);\n  },\n});\n```\n\nWhen `user_version` is lower or higher than `version`, the primitive removes all\nnon-internal schema objects, calls `createSchema`, and stamps the requested\nversion in one transaction. Rebuilding a higher version is intentional: derived\nstores remain usable after an application downgrade.\n\n`fingerprintDerivedSchema()` converts the schema SQL itself into a positive\n31-bit `user_version`. It normalizes formatting whitespace outside quoted SQL\nvalues before computing SHA-256, so reformatting or changing line endings does\nnot rebuild production stores. Meaningful DDL and quoted-value changes do. Pass\nthe exact SQL constant executed by `createSchema`; when schema codegen is added,\ncompute the fingerprint from generator input before formatting generated\nTypeScript.\n\n#### Build-time Drizzle codegen\n\n`@emdash/core/primitives/sqlite-store/codegen` exposes\n`compileDrizzleSchemaToSql(schema)`. It uses `drizzle-kit/api` entirely in\nmemory to diff an empty SQLite snapshot against imported Drizzle table exports.\nThe helper sorts export keys before compilation so generated DDL is stable\nacross module loaders.\n\nKeep this import in build scripts and drift tests only. Runtime stores should\nimport a checked-in generated statement array, compute their fingerprint from\nthat array, and never bundle `drizzle-kit`.\n\n## Driver contract\n\n`SqliteConnection` exposes only:\n\n- `exec(sql)` for DDL and multi-statement SQL;\n- `get`, `all`, and `run` for prepared statements;\n- `close`;\n- `native` as an escape hatch for ORM attachment.\n\nThe store—not the driver—owns pragmas and transaction behavior. Every open sets\nWAL mode, `busy_timeout`, and foreign-key enforcement. Transactions use explicit\n`BEGIN IMMEDIATE`, `COMMIT`, and `ROLLBACK`, so different drivers cannot\nsilently change migration semantics. Transaction callbacks must be synchronous.\n\nUse `runSqliteDriverConformance()` from the testing entry point when adding a\ndriver. It verifies multi-statement execution, parameter binding, result shape,\nTEXT/INTEGER/BLOB/NULL round trips, exact large integers, runner pragmas,\nforeign-key toggling, transaction semantics, and `VACUUM INTO`.\n\n## Durable migrations\n\nA bundled migration contains stable identity and raw SQL:\n\n```ts\ntype BundledMigration = {\n  idx: number;\n  tag: string;\n  when: number; // legacy Drizzle journal milliseconds\n  hash: string; // SHA-256 of raw SQL file bytes\n  sql: string;\n};\n```\n\nThe runner:\n\n1. Bootstraps its bookkeeping schema when `user_version` is zero.\n2. Reads applied tags and verifies hashes for migrations known to this binary.\n3. Computes pending work by tag set difference, ordered by `idx`.\n4. Creates a backup when configured and a persistent database needs work.\n5. Disables foreign keys **before** opening migration transactions.\n6. Applies each migration atomically, splitting Drizzle SQL on\n   `--> statement-breakpoint`.\n7. Records `(tag, hash, applied_at)` in `__emdash_migrations`.\n8. Runs optional interop inside the same transaction.\n9. Runs `PRAGMA foreign_key_check`, then reenables enforcement in `finally`.\n\nRows for migrations unknown to an older binary are ignored. This allows additive\ndatabase upgrades to remain downgrade-tolerant. A known tag with a different\nhash throws because shipped migration SQL is immutable.\n\nDurable migration hashes are SHA-256 of the raw SQL file bytes. The algorithm\nis part of the runner schema contract rather than a per-row `hash_algo` column:\nchanging it requires a `RUNNER_SCHEMA_VERSION` bump that explicitly rewrites\nbookkeeping. Derived fingerprints need no stored algorithm because a changed\nimplementation causes one safe rebuild and then becomes self-consistent.\n\nSQLite ignores `PRAGMA foreign_keys` changes inside a transaction. Migration SQL\nmay contain those pragmas, but correctness comes from the outer runner protocol.\n\n### Legacy migration interop\n\nMigration history conversion is optional and pluggable:\n\n```ts\ntype MigrationInterop = {\n  backfill?(connection, migrations): void;\n  onApplied?(connection, migration): void;\n};\n```\n\n`backfill` runs once inside the bookkeeping bootstrap transaction. `onApplied`\nruns inside each migration transaction after the new bookkeeping row is\ninserted.\n\n`drizzleV0Interop` supports databases written by Drizzle's v0 journal layout. It:\n\n- reads `__drizzle_migrations`;\n- matches rows by `created_at === migration.when`, falling back to SQL hash;\n- throws rather than guessing if a row cannot be identified;\n- dual-writes newly applied migrations to `__drizzle_migrations`.\n\nDual-write keeps older application binaries from reapplying migrations after a\ndowngrade. Configure this interop only for stores with that legacy history.\n\n## Backups and restore\n\nDurable stores with `backup: { retain: N }` create consistent sibling backups\nusing `VACUUM INTO` before pending migrations. Paths are SQL-escaped, and old\nbackups are pruned by the creation timestamp embedded in their filename.\n\nCall `restoreLatestBackup(path)` only when every handle for that store path is\nclosed. Restore removes stale WAL/SHM sidecars and replaces the database through\na temporary file. The method refuses to run while the path is open.\nThat open-path guard is scoped to one store instance; callers must share the\ndefined store rather than independently defining stores for the same file.\n\n## Testing\n\n### Fully migrated temporary database\n\n`openTemp` creates an isolated on-disk database, applies all migrations, awaits\nthe optional seed, and deletes the database plus WAL/SHM files on close:\n\n```ts\nconst handle = await appStore.openTemp(async ({ db }) => {\n  await db.insert(projects).values({ id: 'project-1', name: 'Example' });\n});\n\ntry {\n  // test against handle.db or handle.connection\n} finally {\n  handle.close();\n}\n```\n\nIt is intentionally file-backed rather than `:memory:` so WAL, backup, and\nmulti-connection behavior stay representative.\n\n### Testing a specific migration\n\n`openAtMigration(n)` applies migrations whose `idx < n`. Seed pre-migration data\nwith raw SQL, because latest-schema ORM types cannot faithfully describe an old\nschema:\n\n```ts\nconst handle = appStore.openAtMigration(18);\ntry {\n  handle.connection.exec(`\n    INSERT INTO workspaces (id, type, ssh_connection_id)\n    VALUES ('workspace-1', 'project-ssh', 'missing-ssh')\n  `);\n\n  handle.migrateToLatest();\n  assertSqliteStoreInvariants(handle.connection);\n} finally {\n  handle.close();\n}\n```\n\nThis replaces opaque binary pre-migration fixtures with reviewable, adversarial\ntest setup. `openAtMigration` and backup/restore exist only on the durable store\ntype.\n\n`assertIncrementalMigrationEquivalence(store, migrationCount)` opens every\nhistorical migration boundary, migrates it to latest, and compares its\n`sqlite_schema` with a one-shot latest database. Use it as a compact regression\ntest for the complete migration chain.\n\n## Invariants\n\n- Never edit SQL for a shipped migration. Hash verification will reject it.\n- Hash raw UTF-8 file bytes without trimming or line-ending normalization.\n- Preserve legacy `when` milliseconds while `drizzleV0Interop` is needed.\n- `PRAGMA user_version` belongs exclusively to the store primitive: durable\n  stores use it for runner metadata; derived stores use it for a schema\n  fingerprint.\n- Domain code must not read or write `user_version`.\n- Derived data must be safe to delete at any time.\n- Store initialization after migration throws on integrity failures.\n  `assertSqliteIntegrity` runs SQLite's full B-tree and index consistency check;\n  `assertForeignKeyIntegrity` scans for orphaned references. This intentionally\n  differs from expected operational failures that use `Result`: callers cannot\n  safely continue with an unidentified or corrupt schema.\n","packages/core/src/runtimes/lsp/README.md":"# LSP runtime\n\nBuffers use the host LSP runtime for hover, definition, type definition, references\nand diagnostics, according to each server's capabilities. `typescript-language-server`\n6.0.1 with TypeScript 6.0.3 and Pyright 1.1.414 ship with desktop and workspace-server.\nThey start only when needed; opening a Python file does not launch the TypeScript\nserver. Nothing is downloaded\nwhen a file opens. Other servers can be installed or selected in machine dependencies.\nRight-click a file tab and choose **Language services…** for status and restart;\nthere is no persistent language-service indicator in the file header.\nThe executable runs on the host containing the worktree, including SSH hosts.\nRemote language services are included in workspace protocol 12.0.\n\nThe LSP worker starts on the first language-service request in both desktop and workspace-server\nhosts. Its startup is excluded from required host readiness, so a failed or pending LSP startup does\nnot block unrelated runtimes. A later request can retry failed startup; concurrent requests share\none attempt, and successful readiness stays cached. Individual language servers still start only\nwhen a session is attached.\n\n| Languages | Server | Host requirements |\n| --- | --- | --- |\n| TypeScript, JavaScript, JSX, TSX | [typescript-language-server](https://github.com/typescript-language-server/typescript-language-server) with TypeScript 6 | Bundled, runs with Emdash’s Node/Electron; no external Node required |\n| Bash, sh | [bash-language-server](https://github.com/bash-lsp/bash-language-server) | Node.js 20+; ShellCheck on PATH adds lint diagnostics |\n| Go | [gopls](https://go.dev/gopls/) | Go toolchain; make GOBIN or GOPATH/bin available on PATH after installing |\n| Rust | [rust-analyzer](https://rust-analyzer.github.io/book/installation.html) | Rust toolchain, Cargo and rust-src; the offered rustup command installs the analyzer and sources |\n| Python | [Pyright](https://github.com/microsoft/pyright) | Bundled, runs with Emdash’s Node/Electron; uses the project’s Python environment |\n| C, C++ | [clangd](https://clangd.llvm.org/installation) | Project compiler flags, normally compile_commands.json; Homebrew LLVM's bin directory must be on PATH or selected explicitly |\n| JSON, JSONC | [VS Code JSON server](https://github.com/hrsh7th/vscode-langservers-extracted) | Node.js; JSONC includes .jsonc, tsconfig.json and jsconfig.json |\n| YAML | [yaml-language-server](https://github.com/redhat-developer/yaml-language-server) | Node.js; schemas may be associated using a file modeline |\n| HTML, CSS, SCSS, Less | [VS Code HTML/CSS servers](https://github.com/hrsh7th/vscode-langservers-extracted) | Node.js; HTML, CSS and JSON executables share one npm install package |\n\nBash selection includes `.sh`, `.bash` and known startup files such as `.bashrc`;\nthere is no shebang discovery for arbitrary extensionless scripts. Zsh and Fish\ndo not activate the Bash server. Navigation capabilities vary: for example, schema\nlanguages principally offer hover and validation, not programming-language type\ndefinitions. Monaco retains its existing completion and formatting providers;\nhost-backed LSP completion, formatting and workspace edits are not implemented.\n\nHover and navigation select a provider per document and capability. Missing tools,\nstarting/failed servers, unsupported capabilities and disconnected hosts fall back\nto Monaco's bundled services where available (TS/JS, CSS/SCSS/Less, HTML and JSON).\nA successful empty host answer is authoritative. Local TypeScript validation keeps\nthe editor's existing syntax-only policy. There is one diagnostic marker owner per\nmodel: current host publications, including empty ones, supersede local validation;\nedits clear stale diagnostics, and loss of the host restores local validation. Late\nresults cannot replace a newer document or provider's markers. Disk/Git snapshots\nretain local services and are never replicated to a host.\n\nFallback analyzes loaded models and bundled declarations, not the project's filesystem\nor full configuration. It restores basic hover, definition/reference navigation and\nvalidation where Monaco supports them; type-definition navigation still requires host\nLSP. Rust, Go and other languages without Monaco services still need their host server.\nThe language-services dialog explains basic support and offers host-server retry.\n\nThe Monaco adapter owns provider selection and model lifetimes. Local worker access\nand diagnostic arbitration are separate editor modules, with no host or Wire\nknowledge. Automatic registrations for overlapping Monaco features are disabled;\nthe editor invokes the local workers as fallback. TypeScript reuses Monaco's worker;\nCSS, HTML and JSON fallback workers start lazily and are released after two minutes\nof inactivity. Host availability never changes global language defaults.\n\n[editor/browser/lsp/monaco-language-services.ts](../../../../../apps/emdash-desktop/src/core/features/editor/browser/lsp/monaco-language-services.ts) adapts Monaco models, providers,\ndiagnostics and navigation. It registers providers once during lazy Monaco bootstrap.\n[editor/api/browser/lsp/language-service-client.ts](../../../../../apps/emdash-desktop/src/core/features/editor/api/browser/lsp/language-service-client.ts) owns project discovery, session\nsharing, buffer lifetime, cancellation and synchronization before queries. Its document\nbindings expose semantic queries without requiring Monaco or session keys. File tabs\nregister the originating task and workspace root before their buffer models are created. Only `emdash-buffer:` models are\nreplicated; disk and Git snapshots never enter the language server. Shared model\nlifetime owns open/close, so split panes and dirty buffers surviving tab closure\nremain consistent. `DocumentSynchronizer` coalesces edits and flushes every open\nbuffer before a query. Unchanged versions are checked before reading model text.\nAfter the initial snapshot, the synchronizer sends one compact UTF-16 edit against\nthe last acknowledged version. A mismatched base requests a fresh snapshot;\nreconnects also replay snapshots. The host retains complete text for restart and\nexpands edits only when a server requires full-document synchronization. The shared\nfile store emits successful saves with the exact text written to disk. That immutable\nsnapshot crosses Wire with the save notification; the host uses it for didSave's\noptional text instead of its newer live overlay. Synchronization can keep newer edits\nwithout rolling the server document back. The store leaves those edits dirty and\nrecoverable, and isolates synchronous and asynchronous save-observer failures from\nthe completed write. Results preserve host identity and the originating task when\nnavigating outside the root.\n\n`LanguageSessionClient` leases typed live state through the editor Wire domain.\nIts `attachedClient` promise means the Wire attachment exists; server readiness is\na separate live state. Connection state is separate from authoritative server status.\nIndividual request failures are reported to the caller without marking the server\nas failed or clearing diagnostics. Cancelled and stale queries are discarded. The\nNode editor controller routes to `RuntimeBroker`; it does not own processes or\nLSP protocol state. `packages/core/src/runtimes/lsp/` owns process framing,\ninitialization, document versions, cancellation, capabilities, diagnostics and\nshutdown. It runs as a worker on both desktop and workspace-server, with one\nserver per renderer client, resolved project root and server ID. Server commands come\nfrom bundled assets or host dependency descriptors, never renderer-supplied shell text. A live-state\nlease keeps the server alive; the last detach releases it after a short grace\nperiod. Explicit restart replays unsaved buffers, and replacement generations\ninvalidate renderer synchronization caches.\n\nThe server registry uses UTF-16 positions and a two-million-character document limit. Adding\nanother server means adding portable selection metadata in [lsp/api/server-catalog.ts](api/server-catalog.ts)\nand a host profile in [lsp/node/server-registry.ts](node/server-registry.ts), then exercising its capability\nnegotiation and synchronization behavior. The renderer derives provider selectors,\nprotocol language IDs, session keys and labels from that metadata. Monaco-specific\nlanguage mappings remain in the editor adapter; Core contains only protocol language\nIDs. Host profiles own executable descriptors, arguments, project-root markers and configuration;\nthe process transport only binds their supplied request handlers to JSON-RPC.\n\nProject discovery runs through Wire on the file's host before acquiring a session.\nFor TS/JS, the nearest `tsconfig.json`, `jsconfig.json` or `package.json` between the\nfile and task workspace selects the root; absent markers and external definition\ntargets retain the task workspace root. Automatic selection uses the bundled\n`typescript-language-server`. The TypeScript profile resolves the project's\n`tsserver.js`, including hoisted dependencies, and falls back to the bundled TS6\ncompiler if the project has no compatible server. Resolution does not execute\nproject code. Explicit machine-dependency selections override the bundled launch;\ninvalid selections remain visible as errors. Automatic typing acquisition is\ndisabled so opening a JavaScript file never starts npm downloads. Go recognizes\n`go.mod`/`go.work`; Rust recognizes `Cargo.toml` and\n`rust-project.json`; Python recognizes Pyright, pyproject and common Python project\nfiles; C/C++ recognizes compilation databases, `.clangd` and CMake roots. Each uses\nthe nearest marker inside the task workspace. Bash, JSON, YAML, HTML and CSS use\nthe task workspace root without borrowing a nearby TypeScript package root.\n\nThe Python host profile discovers `.venv`, then `venv`, then a captured `VIRTUAL_ENV`\nwith an executable interpreter, without running project code during discovery.\nOtherwise Pyright discovers Python from the host environment. Pyright continues\nto read `pyrightconfig.json` and `pyproject.toml`; diagnostics are limited to open\nfiles. Go and Rust inherit the captured host toolchain environment. The process\ntransport remains independent of these policies.\nHost configuration is resolved again when a server restarts.\n`vscode-languageserver-protocol` (MIT) supplies JSON-RPC framing and cancellation.\nTypeScript 6.0.3 and `typescript-language-server` 6.0.1 (both Apache-2.0) are pinned\nproduction dependencies, alongside Pyright (MIT). Both servers use the host's\nEmdash Node/Electron executable; Electron launches them with `ELECTRON_RUN_AS_NODE`.\nThe same JavaScript assets work on every supported CPU architecture. Electron\nunpacks the servers and their runtime data, with an explicit resource-copy rule\npreserving TypeScript's `.d.ts` standard libraries, which its default module\nfilter would otherwise remove. Workspace-server stages the same pinned versions.\nRelease validation rejects missing assets or accidental resolution outside the\nproduction deployment. The repository's native-preview compiler remains a\ndevelopment build tool and is not a bundled language server.\n\nAfter Electron finishes packaging, native-architecture release builds also launch\nboth servers using the finished app's executable with an empty PATH and isolated\nhome. The check follows definitions into the shipped TypeScript declarations and\nPyright stubs and verifies type diagnostics. Cross-compiled apps need the same check\non a matching host via `scripts/release/verify-language-servers.ts --executable <binary>\n--resources <resources-directory>`. It does not modify the app or launch its UI.\nThe dedicated CI packaging job exercises stable and canary Electron packages and\nverifies that removing standard-library assets makes the check fail.\nThe probe reuses the existing MIT-licensed LSP protocol client as a direct desktop\ndevelopment dependency instead of implementing a second JSON-RPC transport.\n\nBash, YAML and `vscode-langservers-extracted` remain development-only integration\nfixtures. The extracted web-server package brings TypeScript 4.9.5 transitively;\nit is not shipped as our TypeScript server or as a production dependency. Monaco\nsupplies basic HTML/CSS/JSON support without adding that aggregate server package.\n\nThe Wire contract names snapshot replacement (`setDocumentSnapshot`), versioned\nedits (`applyDocumentEdit`), save notification (`documentSaved`) and process recovery\n(`restartServer`) explicitly. Definition, type-definition and reference queries are\nseparate operations; references preserve the caller's `includeDeclaration` option.\nSnapshot replication is required for initialization and recovery, not compatibility.\nThis unreleased feature is included in protocol 12.0, with no legacy LSP aliases.\n\nTests cover protocol state and actual server behavior in Core, host routing and\nbuffer policy in desktop Node tests, and Monaco providers/navigation in browser\ntests. [language-services.e2e.browser.test.ts](../../../../../apps/emdash-desktop/src/core/features/editor/browser/lsp/language-services.e2e.browser.test.ts) carries actual Wire frames through\nPlaywright bindings into the production editor controller and host runtime,\nthen checks cross-file unsaved types, definitions, diagnostics and restart with\nreal TypeScript and Python servers. The bindings exist only in Vitest's test harness.\nCore's [language-servers.integration.test.ts](node/language-servers.integration.test.ts) additionally checks production host\nprofiles against real Bash, JSON/JSONC, YAML, HTML, CSS/SCSS/Less servers in ordinary\ntest runs. Enable its native Go, Rust and C++ cases with installed `gopls`, `go`,\n`rust-analyzer`, `cargo`, `rustc` and `clangd` on PATH:\n\n```bash\nEMDASH_TEST_NATIVE_LSP=1 pnpm --dir packages/core exec vitest run src/runtimes/lsp/node/language-servers.integration.test.ts\n```\n\nThis opt-in fails if a requested tool is absent; it does not install tools or silently\nskip a broken setup. It covers navigation, type definitions where supported, unsaved\nedits, restart replay and lease cleanup. Schema and stylesheet cases also verify\ndiagnostics arriving and clearing after unsaved edits.\n","packages/shared/README.md":"# @emdash/shared\n\n`@emdash/shared` is the repo's standard library: package-level foundations reused\nby Wire, Core, desktop, workspace-server, and tests. These primitives are\nintentionally generic — they do not define product domains, Wire protocol\nmessages, or host-specific process behavior.\n\n## Standard-Library Stance\n\nWorking primitives keep their exports regardless of measured consumers. A\n`Result` type without `map`/`andThen` is half a type, so the full combinator\nAPI stays exported even when a usage census finds few callers; the same goes\nfor the request middleware family, `createDurableQueue`,\n`createSharedResource`, and the logger utilities. Do not re-litigate\nzero-consumer exports in the next census. What this stance does **not** cover\nis genuine machinery: internals such as timer-delay normalization stay\nunexported.\n\n## The Prelude\n\nThe root entrypoint (`@emdash/shared`) is the single home for the blessed\ncross-cutting core:\n\n- The result module: `Result<T, E>`, `ok`/`err`/`fail`, guards, combinators\n  (`map`, `andThen`, `tryCatch`, `gen`, …), and the serialization/error family\n  (`Serializable`, `SerializedError`, `BaseError`, `toSerializedError`,\n  `resultSchema`).\n- `Unsubscribe` and the lifecycle leases: `Lease`, `PendingLease`,\n  `toPendingLease`, `once`.\n- `createEmitter` and the `Emitter<T>` interface.\n- `isDeepEqual`.\n- `Secret`: `secret`, `isSecret`, `reveal`, `REDACTED`.\n\nEvery symbol has exactly one home package-wide: prelude symbols come from the\nroot, domain symbols from their subpath. Nothing is exported from both.\n\n## Entrypoints\n\n- `@emdash/shared` — the prelude (above).\n- `@emdash/shared/concurrency`: `Scope` (`createScope`), `createLifecycleRegistry`,\n  `createConcurrencyLimiter`, `createKeyedLanes`, `createMailbox`,\n  `createAsyncCache`, `createResourceCache`, `createSharedResource`,\n  `createDurableQueue`, `createBoundedBuffer`, and `Disposable`.\n- `@emdash/shared/scheduling`: `Clock`, `systemClock`, `abortableWait`,\n  `abortReason`, `throwIfAborted`, `waitWithSignal`, `retry`, `retrySchedule`\n  and the `retrySchedules` combinators, `runWithTimeout`, `TimeoutError`, and\n  `TimerHandle`.\n- `@emdash/shared/requests`: request orchestration — `compose` middleware,\n  `withRetry`, `withTimeout`, `withScheduler`, `deduplicate`,\n  `createRequestScheduler`, and `createTokenBucketGate`.\n- `@emdash/shared/logger`: the browser-safe logging surface — `log`,\n  `runWithLogger`, levels, formatting, field preparation, and redaction.\n- `@emdash/shared/logger/node`: the Node-only surface — `initProcessLogging`,\n  `createPinoLogger`, `createFileTransport`, and `installAsyncLogContext`.\n- `@emdash/shared/plugins`: the plugin framework — capabilities, assets,\n  registries, and `PluginIconAsset` (the one icon-asset type; never re-alias it).\n- `@emdash/shared/testing`: `deferred`, `createManualClock`,\n  `createStubLogger`, and `waitFor`.\n- `@emdash/shared/util`: stable utility helpers such as `stableStringify`.\n- `@emdash/shared/markdown`: mention grammar helpers.\n- `@emdash/shared/config`: layered config parsing with zod schemas.\n- `@emdash/shared/perf` and `@emdash/shared/perf/node`: spawn accounting and\n  process vitals instrumentation.\n\n## Choosing Lifecycle Primitives\n\nUse `Scope` for ownership, cancellation, cleanup ordering, and async work that\nmust not outlive its feature. Use `scope.add()` and `scope.use()` for finalizers,\n`scope.child()` for nested ownership, and `scope.run()` for tracked async work.\n\nUse `createLifecycleRegistry` when a feature owns a keyed set of local resources\nwith explicit `start()` and `stop()` commands, typed start/stop results,\nqueryable state, and state-change observers. It is a state registry, not a lease\ncache or protocol primitive.\n\nUse `createResourceCache` when resource lifetime is demand-driven: consumers call\n`acquire()`, hold leases, release them, and optionally benefit from an idle TTL.\nUse `createSharedResource` for the same lease behavior around one unkeyed\nresource, and `createAsyncCache` for retryable cached async values with no\nfinalizer.\n\nThe command/event/effect machine primitive is internal to `@emdash/core`'s acp\nruntime (`packages/core/src/runtimes/acp/node/machine/primitive/`) and is not\noffered as a shared building block. Use Wire-owned primitives when the lifecycle is\nprotocol-specific: Wire workers belong to `@emdash/wire/worker` because they\nsupervise process generations and keep a stable typed Wire client, and\n`LiveJobSource` belongs to `@emdash/wire/live` because it publishes cancellable\njob state, progress, retention, and remote client handles.\n\n## Package Conventions\n\n- **Ownership-drop**: primitives that take ownership of values fire `onDrop`\n  exactly once for every taken-then-discarded value, and never reject because\n  of it.\n- **Never-silent**: where a failure would otherwise vanish, optional failure\n  hooks default to logger-backed reporting rather than swallowing (for example\n  the emitter's `onSubscriberError` and keyed-lane coalesce failures fall back\n  to `log.warn`).\n- **Clock seam**: no raw `Date.now`/`setTimeout` outside `scheduling/clock.ts`;\n  time-dependent primitives accept a `Clock`.\n","packages/wire/docs/README.md":"# @emdash/wire Docs\n\n`@emdash/wire` is the transport-agnostic runtime layer for typed API calls,\nlive model subscriptions, live logs, event streams, jobs, mutations, workers, and\nthe small utilities that sit at the API boundary.\n\nWire builds on Shared foundations for generic lifecycle, scheduling,\nconcurrency, testing, and stable utility behavior:\n\n```mermaid\nflowchart TB\n  subgraph shared [Shared foundations used by Wire]\n    scope[Scope and ResourceCache]\n    registry[LifecycleRegistry]\n    scheduling[Clock and RetrySchedule]\n    mailbox[Mailbox]\n  end\n  subgraph api [API layer]\n    contracts[defineContract endpoint kinds]\n    controller[createController]\n    client[contract client]\n    transports[Transports: memory, port, dom-port, electron, stream, reconnecting]\n  end\n  subgraph live [Live primitives]\n    model[LiveStateSource and replicas]\n    log[LiveLogSource]\n    eventStream[EventStream]\n    job[LiveJobSource]\n    mutations[Mutations and registries]\n  end\n  subgraph workers [Wire workers]\n    component[WireComponent]\n    workerHost[WireWorkerHost]\n  end\n  shared --> api --> live\n  component --> api\n  workerHost --> api\n  workerHost --> component\n```\n\nThe live layer owns the stateful primitives: `LiveStateSource`, `LiveLogSource`,\n`EventStreamSource`, `LiveJobSource`, and consumer-instantiated replicas; live\nmodels are served by providers, usually `expose()` over kernel state. Low-level `*Client` followers track cursors and resync, while\nmaterializers (`StateStore`, `LogSink`, `JobStore`) own values. Most consumers use\nclient handles directly or wrap them in replicas. The API layer turns those\nprimitives into a contract with typed procedure calls and live topic client\nhandles. `WireComponent` is the reusable contract implementation pattern: it\ndeclares explicit typed requirements, validates config at the creation boundary,\nand can be created in-process or hosted in a worker. Worker hosting is Wire-specific because it\nserves components across processes. Generic lifecycle, scheduling, concurrency, testing, and stable\nutility primitives live in `@emdash/shared` and are documented here where Wire\nuses them. The `WireInstrumentation` seam is cross-cutting and can be attached\nto API, live, and worker surfaces through their options.\n\n## Pages\n\n- API:\n  - [Contracts](./api/contracts.md): `defineContract()`, endpoint kinds, nested\n    composition, and live model groups.\n  - [Serving and clients](./api/serving.md): `createController()`, `serve()`,\n    `connect()`, cancellation, controller composition, session hubs, and\n    server-side call helpers.\n  - [Composable middleware](./api/middleware.md): target-first `compose()`,\n    handler middleware, controller middleware, timeout, retry, and\n    deduplication.\n  - [Typed clients](./api/clients.md): `ContractClient` handles,\n    `forwardController()`, and selective forwarding through `createController()`.\n  - [File endpoints](./api/files.md): `downloadFile()`, `uploadFile()`, blob\n    channels, and binary stream transport framing.\n  - [Wire errors](./api/errors.md): error planes, `WireErrorCode` meanings,\n    origins, and retry guidance.\n  - [Transports](./api/transports.md): memory, ports, Electron, streams, and\n    reconnecting transports.\n- Live:\n  - [Live models and protocol](./live/live-model.md): snapshots, updates,\n    cursors, `LiveStateSource`, and replicas.\n  - [Live logs](./live/live-log.md): retained terminal-style logs and client\n    callbacks.\n  - [Event streams](./live/event-stream.md): keyed fire-and-forget events with\n    explicit gap callbacks after reattach.\n  - [Live jobs](./live/live-job.md): progress, cancellation, terminal state,\n    retention, and contract job handles.\n  - [Mutations](./live/mutations.md): mutation ids, host contexts, cursor settling,\n    idempotency cache, and retry behavior.\n  - [Replicas](./live/replicas.md): `LiveModelReplicaCache`, `LiveLogReplicaCache`,\n    `LiveJobReplicaCache`, pluggable stores, ref counting, and serving cached state.\n- Runtime:\n  - [Lifecycle utilities](./runtime/lifecycle.md): Shared `Scope`,\n    `LifecycleRegistry`, scope loggers, `describeScope()`, and resource\n    ownership.\n  - [Structured concurrency](./runtime/structured-concurrency.md): `Scope.run()`,\n    run cancellation, lifecycle invariants, and diagnostics.\n  - [Scheduling](./runtime/scheduling.md): Shared `Clock`, `TimerHandle`,\n    `ManualClock`, retry schedules, abortable sleeps, and timer ownership.\n  - [Resource caches](./runtime/resource-cache.md): Shared `ResourceCache`,\n    `SharedResource`, `AsyncCache`, identity rules, and lease behavior.\n  - [Mailbox and Broadcast](./runtime/mailbox-and-broadcast.md): Shared bounded\n    local async handoff, overflow policy, guarantees, and the deferred Broadcast\n    contract.\n  - [Components](./runtime/components.md): `defineWireComponent()`, explicit\n    requirements, in-process creation, worker deployment, and non-DI composition\n    rules.\n  - [Workers](./runtime/workers.md): `WireWorkerHost`, one-generation spawners,\n    `runWireComponentWorker()`, and process-hosted components.\n- [Observability](./observability.md): ambient logger context, the\n  `WireInstrumentation` seam, and scope loggers.\n\nRunnable examples live under [../examples](../examples). Most snippets in these\ndocs are shortened versions of those files.\n\n## Package Exports\n\nThere is no root export: every symbol has exactly one home in a hand-curated\nsubpath entrypoint.\n\n```ts\nimport { createController, defineContract } from '@emdash/wire/rpc';\nimport { cell, expose } from '@emdash/wire/state';\n```\n\n- `@emdash/wire/rpc`: contract definition (`defineContract()`, endpoint\n  factories), controller creation, clients, `connect()`/`serve()`, transports,\n  protocol vocabulary (`WireMessage`, `WireTransport`, `WireError`,\n  `LiveSource`, `LiveUpdate`, cursors), provider seam types\n  (`LiveModelProvider`, `LeasedLiveModelProvider`,\n  `LiveModelMutationEnvelope`), the blob/file surface, validation, and the\n  `WireInstrumentation` seam types. Retry schedules come from\n  `@emdash/shared/scheduling`.\n- `@emdash/wire/live`: the server-side reactivity sources (`LiveLogSource`,\n  `LiveJobSource`, `EventStreamSource`, `createEventStreamHost()`), the keyed\n  replica caches (`createLiveLogReplicaCache()`, `createLiveJobReplicaCache()`,\n  `ReplicaLog`), and resync failure policies (`resyncRetry()`,\n  `resyncMarkStale()`).\n- `@emdash/wire/state`: state kernel primitives and Wire bridges (`cell`,\n  `query`, `family`, `optimistic`, `observe`, `expose`, `remote`).\n- `@emdash/wire/mobx`: MobX-backed log/store helpers\n  (`createImmutableMobxStore`, `createReactiveMobxStore`, `createMobxLogStore`).\n- `@emdash/wire/testing`: Wire test helpers such as `createTestWire()` and fake\n  worker process support.\n- `@emdash/wire/worker`: `WireWorkerHost`, `runWireComponentWorker()`,\n  `defineWireComponent()` and component requirement helpers, worker signal\n  types, supervision types, process contracts, and worker log forwarding.\n- `@emdash/wire/worker/node`: Node `childProcessSpawner()`.\n\nUse Shared subpaths directly for generic foundations:\n\n- `@emdash/shared/concurrency`: `Scope`, `Run`, `LifecycleRegistry`, `Mailbox`,\n  `ResourceCache`, `SharedResource`, `AsyncCache`, bounded buffers, and disposable\n  helpers.\n- `@emdash/shared/scheduling`: `Clock`, `systemClock`, `TimerHandle`,\n  `TimeoutError`, `runWithTimeout()`, `RetrySchedule`, retry schedule builders,\n  and `retry()`.\n- `@emdash/shared/requests`: request handler composition, timeout, retry,\n  in-flight deduplication, and request scheduling.\n- `@emdash/shared/testing`: `createManualClock()`, `deferred()`, `waitFor()`, and\n  stub logger helpers.\n- `@emdash/shared/util`: stable utility helpers such as `stableStringify()`.\n\nMobX-backed utilities intentionally live in their own export because they have a\n`mobx` peer dependency. Server-only code can import `@emdash/wire/rpc`,\n`@emdash/wire/live`, `@emdash/wire/state`, `@emdash/wire/worker`, and Shared\nfoundation subpaths without pulling in MobX.\n\n## Typical Flow\n\n1. Define a contract with `defineContract({ ... })`.\n2. Create server-side `cell`, `query`, `LiveLogSource`, `EventStreamSource`, or `LiveJobSource`\n   instances and publish live models with `expose()`.\n3. Use `family()` or explicit domain indexes for keyed live model resources.\n4. Create a controller with `createController(contract, impl)`. Validation is\n   applied by default (`'full'` outside production, `'inputs'` in production);\n   pass `{ validate }` to override.\n5. Serve the controller over a `WireTransport`.\n6. Connect from the client and create a typed `client()`.\n7. Use client handles directly for streaming, or use `remote()` for local state,\n   ref counting, and downstream observation.\n\nFor a complete state kernel example (`cell` + `expose` on the server,\n`remote()` on the client), see [../examples/state-kernel](../examples/state-kernel).\n\n## Event Stream Attachment Law\n\nEvent streams retain no events, and a connection deduplicates attachments to the same topic.\nAnything a late attacher must learn therefore has to live in a retained snapshot or in the attach\nacknowledgment, never in an event-stream message. A one-shot `ready` event is not a valid readiness\nbarrier: a consumer joining an already-attached topic will not cause the server to emit it again.\n\nUse `resourcedStream()` when a successful attach must guarantee that an underlying resource is\nready. Its host must be created with `createEventStreamHost(def, { activate })`, where `activate`\nreceives an `AbortSignal` and resolves to the activation-owned disposer only after the resource is\nready. The source registers the subscriber before activation begins, shares and retains the\nactivation promise for all subscribers to the key, and invokes the disposer when the final\nsubscriber leaves. If the final subscriber leaves while activation is pending, the signal aborts\nand any late result is disposed. Failed activation is not retained, so a later attach makes a fresh\nattempt.\n\nThis guarantee is enforced when a controller binds the endpoint: a resourced definition accepts\nits matching host or a forwarded client handle, but rejects a bare resolver. Forwarding preserves\nthe barrier because the downstream attach is acknowledged only after the upstream attach is\nacknowledged.\n","packages/wire/docs/api/clients.md":"# Typed Clients\n\n`client(contract, connection)` creates a typed `ContractClient`. It has the\nsame nested shape as the contract, but it does not materialize live state.\n\nThink of it as a type-safe wrapper around `Connection`: it knows contract paths,\ninput/output types, live topic ids, group keys, and mutation envelopes, but it does\nnot keep snapshots, apply patches, or settle mutations against local state.\n\n```ts\nconst contractClient = client(api, connect(transport));\n\nawait contractClient.ping({ message: 'hello' });\n\nconst state = contractClient.conversation.state(key, 'state');\nconst snapshot = await state.snapshot();\nconst detach = await state.attach((update) => {\n  console.log(update.delta);\n});\n```\n\n## Endpoint Shapes\n\nEach contract endpoint becomes a protocol client handle:\n\n- Procedures become typed functions: `contractClient.ping(input, { signal? })`.\n- Live logs become `LiveLogClientHandle`s: `contractClient.output.handle(key)`.\n- Event streams become `EventStreamClientHandle`s:\n  `await contractClient.events.subscribe(key, { onEvent, onGap?, onError? })`.\n- Live models become `LiveModelClientHandle`s:\n  `contractClient.conversation.state(key, 'state')` and\n  `contractClient.conversation.mutate('setTitle', envelope)`.\n- Jobs become `LiveJobClientHandle`s: `contractClient.build.start(input)`,\n  `contractClient.build.cancel(jobId)`, and `contractClient.build.handle(jobId)`.\n\n`LiveClientHandle` is the common live handle:\n\n```ts\ntype LiveClientHandle<T> = {\n  topic: string;\n  snapshot(): Promise<LiveSnapshot<T>>;\n  attach(\n    push: (update: LiveUpdate) => void,\n    options?: {\n      onReattach?: () => void;\n      onReattachError?: (error: WireError, context: { retrying: boolean }) => void;\n    }\n  ): Promise<Unsubscribe>;\n  asLiveSource(): LiveSource;\n};\n```\n\n`snapshot()` and `attach()` are useful for low-level code. For example, PTY output\ncan stream directly into an xterm instance without allocating another store:\n\n```ts\nconst output = contractClient.ptyOutput.handle({ sessionId });\nterm.write((await output.snapshot()).data.text);\n\nconst detach = await output.attach((update) => {\n  term.write((update.delta as { chunk: string }).chunk);\n}, {\n  onReattach: async () => {\n    term.reset();\n    term.write((await output.snapshot()).data.text);\n  },\n  onReattachError: (error, { retrying }) => {\n    if (!retrying) showOutputError(error.message);\n  },\n});\n```\n\nWhen a process wants local state, pass the client handle to a replica wrapper instead.\n\n## Forwarding\n\nContract clients are intentionally forwardable. Use `forwardController(contract, client)`\nwhen a hop should serve the same contract without owning state or intercepting\nindividual endpoints:\n\n```ts\nconst upstream = client(api, connect(sshTransport));\nconst controller = forwardController(api, upstream);\n```\n\nNo live state is created at that hop. The downstream client sees the same contract,\nwhile calls, snapshots, live attachments, jobs, and group mutations are forwarded to\nthe upstream connection. Event stream handles are forwarded as live attachments with\nthe same at-most-once delivery semantics as the upstream connection.\n\nForwarding also preserves upstream live lifecycle. A downstream subscriber receives\n`onGap` after the upstream attachment has reattached, and receives `onError` when\nan upstream reattach fails, even if the downstream transport never disconnected.\n\nThis is the right shape for protocol relays and middle tiers that should not own or\ncache state. The hop does not create `ReplicaState`s, does not allocate local\ncursor spaces, and does not run mutation settling. It just preserves the contract\nsurface while delegating to the upstream connection.\n\n`forwardController()` is the preferred shape for full protocol relays because it\nderives the controller implementation from the contract. Adding a new endpoint to\nthe contract does not require a hand-written forwarding entry at each relay. File\ndownloads are adapted internally from the client download handle back into the\ncontroller's streamed download shape.\n\nUse `createController()` directly when forwarding should be selective. A procedure\nmethod can be passed as a procedure implementation, and live client handles can be\npassed as live model, live log, event stream, or live job implementations:\n\n```ts\nconst upstream = client(workspaceApi, connect(sshTransport));\n\nconst controller = createController(workspaceApi, {\n  // Local interception.\n  ping: async () => 'desktop-main',\n\n  // Forwarded subtree.\n  git: upstream.git,\n\n  // Cached group; see Replicas.\n  conversation: createLiveModelReplicaCache(\n    workspaceApi.conversation,\n    upstream.conversation,\n    { lingerMs: 10 * 60_000 }\n  ),\n});\n```\n\nUse this composition style for middle tiers that need local handlers, auditing,\nauthorization checks, replicas, or other endpoint-specific behavior. The explicit\ncontroller implementation makes the interception points visible.\n\n## When to Use Replicas\n\nUse replicas when a process wants local state. See [Replicas](../live/replicas.md).\n\nCommon examples:\n\n- A renderer window wants observable UI state.\n- Electron main wants state that survives renderer reloads.\n- A process wants a local cache before serving the same live model contract to\n  downstream clients.\n\nFor Electron main and other middle tiers that both serve and inspect live state, use\nthe replica local access pattern in\n[Replicas](../live/replicas.md#model-replicas).\n\nDo not materialize just to forward. Forward the client handle or subtree directly.\n","packages/wire/docs/api/contracts.md":"# API Contracts\n\nContracts describe the API surface once. Controller creation and client creation\nderive their types from the same object.\n\n## Defining a Contract\n\nUse `defineContract({ ... })`. Object keys are significant: they determine\nprocedure paths and live ref ids after nested contracts are mounted.\n\n```ts\nexport const notesApi = defineContract({\n  activity: liveLog({ key: sessionKeySchema }),\n  session: liveModel({\n    key: sessionKeySchema,\n    states: {\n      notes: liveState({ data: notesStateSchema }),\n    },\n    mutations: {\n      addNote: mutation(\n        { input: z.object({ text: z.string() }), data: noteSchema, error: z.string() },\n        (ctx, input) => {\n          const note = { id: input.text.toLowerCase(), text: input.text };\n          ctx.produce('notes', (draft) => {\n            draft.notes.push(note);\n          });\n          return ok(note);\n        }\n      ),\n    },\n  }),\n  clearNotes: procedure({\n    input: sessionKeySchema,\n    output: notesStateSchema,\n  }),\n});\n```\n\nSee [../../examples/api-definition/contract.ts](../../examples/api-definition/contract.ts).\n\n## Endpoint Kinds\n\n### `procedure`\n\n`procedure({ input, output })` defines a request/response call. Procedure clients\naccept an optional `{ signal }` call option for cooperative cancellation; see\n[serving](./serving.md#cancellation).\n\n### `fallible`\n\n`fallible({ input, data, error })` defines a procedure whose output is a\n`Result<data, error>` payload:\n\n```ts\nloadNote: fallible({\n  input: z.object({ id: z.string() }),\n  data: noteSchema,\n  error: z.object({ type: z.literal('note_not_found') }),\n});\n```\n\nUse `fallible()` for expected domain failures that callers should handle as data.\nThrown `WireError`s are reserved for infrastructure failures and bugs such as\ndisconnects, unknown paths, and uncaught handler exceptions. See\n[wire errors](./errors.md).\n\n### `mutation`\n\n`mutation({ input, data, error }, handler?)` defines a live-state mutation shape.\nMutations are members of `liveModel()` definitions. Optimistic previews should use\nshared reducer functions with the `optimistic()` bridge rather than contract-local\nclient models.\n\n`mutation()` is for operations that must settle live model cursors. Use\n`procedure()` for calls that do not update live models and `liveJob()` for\nlong-running work.\n\n### `liveLog`\n\n`liveLog({ key })` declares a keyed text log endpoint. It is served by a\n`LiveLogSource` resolver and consumed through a `LiveLogClientHandle` or a\n`LiveLogReplicaCache`.\n\n### `eventStream`\n\n`eventStream({ key, event })` declares a keyed, fire-and-forget event channel.\nEvents are delivered only while a client is attached; the server keeps no\nretained snapshot and late subscribers do not receive historical events.\n\n```ts\nfileEvents: eventStream({\n  key: z.object({ rootPath: z.string() }),\n  event: z.object({\n    kind: z.enum(['create', 'update', 'delete']),\n    path: z.string(),\n  }),\n});\n```\n\nServe event streams with `createEventStreamHost(contract.fileEvents)` or a\nresolver that returns an `EventStreamSource`. Client handles expose\n`await subscribe(key, { onEvent, onGap?, onError? })`. `onGap` runs after\ntransport reattach so consumers can resync state that may have missed events.\nUse `eventStream` for loss-tolerant notifications and invalidation signals; use\n`liveLog` when callers need retained text output and `liveModel` when callers\nneed convergent state. See [event streams](../live/event-stream.md).\n\n### `liveJob`\n\n`liveJob({ input, progress, result, error })` models long-running work with progress,\ncancellation, terminal state, and reattach:\n\n```ts\nbuild: liveJob({\n  input: z.object({ target: z.string() }),\n  progress: z.object({ step: z.string() }),\n  result: z.object({ artifact: z.string() }),\n  error: z.object({ message: z.string() }),\n});\n```\n\n`createController()` binds the endpoint to `{ run, toError? }`. `run` returns a\n`Result<result, error>` payload; `toError` is only an optional fallback for\nunexpected thrown errors. The typed client gets `start(input)`, `cancel(jobId)`,\nand `handle(jobId)` helpers. See\n[live jobs](../live/live-job.md).\n\n### `downloadFile` and `uploadFile`\n\n`downloadFile({ input, meta?, error })` and\n`uploadFile({ input, accept?, maxSize?, result, error })` model byte transfer as a\ntyped call envelope plus a credit-based blob channel. Bytes are streamed in\nbounded chunks and are not embedded in procedure inputs or results:\n\n```ts\nreadAttachment: downloadFile({\n  input: z.object({ id: z.string() }),\n  error: z.object({ type: z.string() }),\n});\n\nuploadAttachment: uploadFile({\n  input: z.object({ conversationId: z.string() }),\n  accept: ['image/png', 'image/jpeg'],\n  maxSize: 25 * 1024 * 1024,\n  result: attachmentRefSchema,\n  error: z.object({ type: z.string() }),\n});\n```\n\nDownload clients receive a single-use handle with `chunks()`, `bytes()`, `file()`,\nand `cancel()`. Upload clients pass a file-like value as the second argument. See\n[file endpoints](./files.md).\n\n### `liveModel`\n\n`liveModel({ key, states, mutations })` aggregates related live states and the\nmutations that may touch them. A single key addresses every member. States are\ndeclared with `liveState({ data })`; the helper creates keyed member refs.\n\n```ts\nconst api = defineContract({\n  conversation: liveModel({\n    key: conversationKeySchema,\n    states: {\n      state: liveState({ data: stateSchema }),\n      usage: liveState({ data: usageSchema }),\n    },\n    mutations: {\n      setTitle: mutation(\n        { input: z.object({ title: z.string() }), data: z.void(), error: z.string() },\n        (ctx, input) => {\n          ctx.produce('state', (draft) => {\n            (draft as { title: string }).title = input.title;\n          });\n          ctx.produce('usage', (draft) => {\n            (draft as { tokens: number }).tokens += input.title.length;\n          });\n          return ok(undefined);\n        }\n      ),\n    },\n  }),\n});\n```\n\nInline group mutation handlers should be pure functions of the member drafts and\ninput: avoid I/O, time, randomness, and server-only state in the inline handler\nbody. If a mutation is schema-only (`mutation(def)`), the server supplies the\nhandler when it creates the live model host.\n\n## Nested Composition\n\nContracts can contain other contracts:\n\n```ts\nconst ptyAgent = defineContract({\n  sessions: liveModel({\n    key: sessionKeySchema,\n    states: { state: liveState({ data: sessionStateSchema }) },\n  }),\n  output: liveLog({ key: sessionKeySchema }),\n});\n\nconst api = defineContract({ ptyAgent });\n```\n\nThe final mount path determines ids and procedure paths:\n\n- `api.ptyAgent.sessions.states.state.id` is `ptyAgent.sessions.state`.\n- `api.ptyAgent.output.id` is `ptyAgent.output`.\n- a procedure at `api.ptyAgent.startSession` is called as `ptyAgent.startSession`.\n- a group member at `api.tasks.conversation.states.state` gets the id\n  `tasks.conversation.state`.\n\nThis lets packages define small contracts locally and compose them into a larger\nworkspace API without an extra namespace argument.\n\n## Live Model Providers\n\nOn the server, implement a live model endpoint with a `LiveModelProvider`. The\nusual way to build one is `expose()` from `@emdash/wire/state`, which resolves\neach contract state to a kernel readable per key:\n\n```ts\nconst conversations = expose(\n  api.conversation,\n  {\n    state: (key) => conversationState(key),\n    usage: (key) => conversationUsage(key),\n  },\n  { mutations }\n);\n```\n\nThen bind the endpoint by passing the provider in the implementation object:\n\n```ts\nconst controller = createController(api, { conversation: conversations });\n```\n\nThe live model client handle exposes `state(key, name)` for member handles and\n`mutate(name, envelope)` for mutation calls. Wrap it in `createLiveModelReplicaCache()`\nwhen a process wants local state, ref counting, and mutation settling. See\n[serving](./serving.md#typed-clients).\n","packages/wire/docs/api/errors.md":"# Wire Errors\n\nWire has two error planes:\n\n- Expected domain failures travel as `Result` payloads. Prefer `fallible()` for\n  procedure contracts whose callers should handle failures as ordinary data.\n- Thrown `WireError`s are reserved for infrastructure failures, contract drift,\n  lifecycle mistakes, and uncaught handler exceptions.\n\n`WireError` instances carry a typed `code`, a message, and an optional serialized\ncause. When an uncaught JavaScript error crosses the wire, the transport serializes\nit as `HANDLER_ERROR` with a clone-safe cause.\n\n## Codes\n\n### `CANCELLED`\n\nThe call was aborted. The client raises it when the caller's `AbortSignal` is\nalready aborted or aborts while a call is pending. The server raises it when it\nreceives a `cancel` message and the handler observes the abort.\n\nRetrying is not automatic and usually not appropriate because cancellation was\ncaller intent.\n\n### `DISCONNECTED`\n\nThe transport disconnected while a request was in flight, posting a request\nfailed because the peer was already gone, the held-call buffer overflowed while\ndisconnected, or the transport failed permanently (the terminal cause is\nattached as `cause`).\n\nMutations are never retried automatically. Callers can opt in per call with\n`retry: { schedule }`; that retry is safe because mutations carry a stable\n`mutationId` and the server deduplicates repeated ids.\n\n### `UNKNOWN_PROCEDURE`\n\nNo procedure handler exists for the call path. Common causes are an incorrect\nclient path, a missing implementation branch in `createController()`, or a drift\nbetween the contract and the controller implementation.\n\nThis is not retryable without changing code or wiring.\n\n### `UNKNOWN_TOPIC`\n\nThe live topic's ref id is not registered by the controller.\n\nThis usually means the client encoded the wrong live ref id, or a controller was\ncreated without the endpoint that owns the ref. It is not retryable without\nchanging the topic or server wiring.\n\n### `NOT_FOUND`\n\nThe live ref id is known, but no live instance exists for the provided key.\nExamples include a live model group instance that has not been created yet or a\njob state that has already been evicted.\n\nRetrying may make sense only if the domain resource is expected to appear later.\n\n### `MISSING_HANDLER`\n\nController creation-time error: a contract endpoint has no implementation. Examples include a\nmissing procedure implementation, missing live resolver, missing job handler, or\nmissing live model provider.\n\nThis should not cross the wire during normal operation. Fix the controller implementation.\n\n### `CONTRACT_MISMATCH`\n\nController creation-time error: a live model provider, replica cache, or client\nhandle was created for a different contract than the endpoint being bound.\n\nThis is not retryable; fix the provider/contract pairing.\n\n### `ALREADY_EXISTS`\n\nA keyed live resource was created for a key that already has a live instance.\n\nThis is a lifecycle error. Dispose or reuse the existing instance instead of\ncreating another one for the same key.\n\n### `TIMEOUT`\n\nA Wire call or snapshot exceeded an infrastructure deadline. The connection\napplies a default 30s call deadline (`callTimeoutMs`, per-call `timeoutMs`\noverride) spanning call-issued to result-received, including time spent held\nwhile disconnected; live attach traffic and blob streaming are exempt. Timeout\nis distinct from `CANCELLED`: cancellation is caller intent, while timeout means\npolicy stopped waiting for the operation.\n\nTimeouts are not retried automatically. Mutation calls that opt in to\n`retry: { schedule }` retry both `DISCONNECTED` and `TIMEOUT`, relying on\nstable `mutationId` deduplication. If a domain treats a timeout as an expected\noutcome, model it as a `Result` payload instead of relying on the\ninfrastructure error plane.\n\n### `HANDLER_ERROR`\n\nAn uncaught exception escaped a server handler or validation boundary. Expected\ndomain failures should be modeled as `Result` payloads with `fallible()` instead\nof thrown.\n\nThe serialized wire error includes the thrown value as a clone-safe cause. Treat\nthis as a bug signal and fix the handler or validation logic.\n\n## Live Attachment Errors\n\nLive attachments use the same infrastructure error plane as calls and snapshots.\nThey do not wrap startup or lifecycle failures in endpoint `Result` payloads.\n\nInitial attachment failures reject the `attach()` or event-stream `subscribe()`\npromise with a `WireError`. Common codes are `DISCONNECTED`, `UNKNOWN_TOPIC`,\n`NOT_FOUND`, and `HANDLER_ERROR`.\n\nOnce an attachment is established, the client keeps it as durable intent across\ntransport reconnects. Reattach failures are delivered to the subscription's\n`onReattachError` or `onError` callback with `{ retrying }`:\n\n- `DISCONNECTED` is reported with `retrying: true`; the attachment stays\n  registered and will try again on the next reconnect.\n- Other `WireError`s are reported with `retrying: false`; the attachment is\n  terminated because retrying the same topic would repeat the same failure.\n\nForwarding preserves these lifecycle notifications. A downstream connection can\nremain healthy while an upstream live attachment reports a gap or terminal error.\nForwarding sends those as live lifecycle messages rather than hiding them behind\nthe downstream transport state.\n\nThese errors are distinct from expected domain events carried by a stream. For\nexample, fs-watch may emit `{ kind: 'error' }` as part of its event payload when a\nnative watcher fails to start. That payload belongs to the fs-watch contract. A\nlive attachment `WireError` means the wire topic itself could not attach or\nreattach.\n","packages/wire/docs/api/files.md":"# File Endpoints\n\n`downloadFile()` and `uploadFile()` stream bytes through a dedicated blob channel\ninstead of embedding `Uint8Array` payloads in procedure inputs or results. The call\npayload carries only typed input, metadata, and a channel id; byte chunks flow after\nthat over credit-based frames.\n\n## Contracts\n\n```ts\nconst api = defineContract({\n  readAttachment: downloadFile({\n    input: z.object({ id: z.string() }),\n    error: z.object({ type: z.string() }),\n  }),\n  uploadAttachment: uploadFile({\n    input: z.object({ conversationId: z.string() }),\n    accept: ['image/png', 'image/jpeg'],\n    maxSize: 25 * 1024 * 1024,\n    result: attachmentRefSchema,\n    error: z.object({ type: z.string() }),\n  }),\n});\n```\n\nFile metadata uses `{ name, mimeType, size?, lastModified? }`. Download endpoints\nmay extend the metadata schema with endpoint-specific fields.\n\n## Server Implementations\n\n```ts\nconst controller = createController(api, {\n  readAttachment: async ({ id }) => {\n    const stored = await store.get(id);\n    if (!stored) return err({ type: 'missing' });\n    return ok({\n      meta: {\n        name: stored.ref.name,\n        mimeType: stored.ref.mimeType,\n        size: stored.data.byteLength,\n      },\n      source: stored.stream(),\n    });\n  },\n  uploadAttachment: async (input, file) => {\n    const bytes = await file.bytes();\n    return ok(await store.put({ data: bytes, mimeType: file.mimeType, name: file.name }));\n  },\n});\n```\n\nThe source can be an `AsyncIterable<Uint8Array>` or a readable stream-like value.\n`WireFile` on upload exposes `stream()`, `bytes()`, `file()`, and `cancel()`.\n\n## Client Usage\n\n```ts\nconst downloaded = await client.readAttachment({ id });\nif (downloaded.success) {\n  for await (const chunk of downloaded.data.chunks()) {\n    // incremental processing\n  }\n}\n\nconst uploaded = await client.uploadAttachment({ conversationId }, file);\n```\n\nDownload handles are single-consumption: choose `chunks()`, `bytes()`, or `file()`.\n`bytes()` verifies `meta.size` when present.\n\n## Protocol Invariants\n\n- Bytes are sent as `blob-chunk` frames and never inside `call`/`result` values.\n- Producers send only after the consumer grants credit with `blob-pull`.\n- Consumers keep a bounded local chunk mailbox aligned with the maximum granted\n  credit. Overflow is treated as a protocol failure; blob chunks are never\n  dropped.\n- `blob-end` is the only successful EOF signal; disconnect before it is an error.\n- `blob-close` cancels a channel and disposes the producer source.\n- Frames for unknown channels are ignored so close/chunk races are harmless.\n- Chunks are re-sliced to standalone buffers before posting to avoid cloning a much\n  larger backing `ArrayBuffer`.\n\nStructured-clone transports carry `Uint8Array` chunks natively. `streamTransport`\nuses length-prefixed binary framing so workspace-server stdio/socket/SSH paths send\nraw bytes with no base64 expansion.\n","packages/wire/docs/api/middleware.md":"# Composable Middleware\n\nWire uses explicit composition for execution policy. Contracts describe protocol\nshape; middleware wraps handlers or controllers at the composition site.\n\n```ts\nimport { compose, deduplicate, withRetry, withTimeout } from '@emdash/shared/requests';\nimport { retrySchedules } from '@emdash/shared/scheduling';\nimport { createController } from '@emdash/wire/rpc';\n\nconst loadStats = compose(\n  async (input: { repo: string }, meta: { signal?: AbortSignal }) => {\n    return await fetchStats(input.repo, { signal: meta.signal });\n  },\n  [\n    withTimeout({ timeoutMs: 20_000 }),\n    withRetry({\n      schedule: retrySchedules.exponential({\n        initialMs: 250,\n        maxMs: 2_000,\n        maxRetries: 3,\n      }),\n      shouldRetry: isTransient,\n    }),\n    withTimeout({ timeoutMs: 5_000 }),\n  ]\n);\n\nconst controller = createController(api, { loadStats });\n```\n\n## `compose()`\n\n`compose(target, middlewares)` accepts the value being wrapped first and a\nreadonly middleware array second:\n\n```ts\ntype Middleware<T> = (next: T) => T;\n```\n\nThe first array entry is outermost. It sees the request first and settles last:\n\n```mermaid\nflowchart LR\n  composeCall[\"compose(handler, middlewares)\"] --> totalTimeout[TotalTimeout]\n  totalTimeout --> retry[Retry]\n  retry --> attemptTimeout[AttemptTimeout]\n  attemptTimeout --> handler[RawHandler]\n```\n\nThis order makes policy boundaries explicit. In the first example, the outer\ntimeout bounds the complete retry program, while the inner timeout bounds each\nindividual attempt.\n\n## Handler Middleware\n\nHandler middleware wraps procedure implementations before `createController()`:\n\n```ts\nconst handler = compose(rawHandler, [\n  withTimeout({ timeoutMs: 10_000 }),\n  deduplicate({ key: (input) => input.repo }),\n]);\n```\n\nUse handler middleware for endpoint-specific execution policy:\n\n- Timeout and deadline policy.\n- Retry policy for idempotent or externally safe operations.\n- In-flight request deduplication.\n- Per-endpoint rate limiting or concurrency limits when those utilities exist.\n\nProcedure handlers receive `(input, meta)`. Middleware must preserve every field\non `meta` and replace only `meta.signal` when it needs derived cancellation:\n\n```ts\nconst wrapped = async (input, meta) => {\n  return await next(input, { ...meta, signal: derivedSignal });\n};\n```\n\nMiddleware functions are intentionally not contract annotations. Two endpoints can\nshare the same schema and still have different execution policy depending on\nwhere they are served.\n\n## Controller Middleware\n\nController middleware wraps a complete `Controller` and satisfies\n`Middleware<Controller>` from `@emdash/shared/requests`. The seam exists as the\nsanctioned extension point for future cross-cutting concerns (for example\nobservability), even though Wire ships no controller middleware today.\n\nContract validation is not middleware: it is a `createController()` option.\nEvery controller validates by default — `'full'` (inputs + outputs) outside\nproduction, `'inputs'` in production — with an explicit `validate` override:\n\n```ts\nconst controller = createController(api, impl, { validate: 'inputs' });\n```\n\nUse controller middleware for boundary-wide behavior:\n\n- Logging and request instrumentation.\n- Session or authorization checks that apply to every procedure on the served\n  controller.\n\nPrefer handler middleware when the policy belongs to one endpoint. Prefer\ncontroller middleware when the policy belongs to the served boundary.\n\n## Timeout\n\n`withTimeout({ timeoutMs, clock? })` from `@emdash/shared/requests` is handler\nmiddleware backed by Shared `runWithTimeout()` from `@emdash/shared/scheduling`.\nIt derives a child `AbortSignal`, passes that signal to the wrapped handler, and\nconverts an expired deadline to a timeout error.\n\n```ts\nconst startSession = compose(rawStartSession, [\n  withTimeout({ timeoutMs: 10_000, clock }),\n]);\n```\n\nTimeout is infrastructure failure, not a domain result. If callers should handle\na timeout as expected data, model it in the endpoint's `Result` error schema\ninstead.\n\nCaller cancellation still wins over timeout. If the caller aborts the request,\nthe wire call rejects with `CANCELLED`.\n\n## Retry\n\n`withRetry()` from `@emdash/shared/requests` is handler middleware backed by\nShared `retry()` from `@emdash/shared/scheduling`. It requires an explicit\n`shouldRetry` classifier:\n\n```ts\nconst loadRemote = compose(rawLoadRemote, [\n  withRetry({\n    schedule: retrySchedules.fixed(250, 2),\n    shouldRetry: (error) => isWireError(error, 'DISCONNECTED'),\n  }),\n]);\n```\n\nDo not retry arbitrary procedure failures. A retry policy should be attached only\nwhen the operation is idempotent, deduplicated by request identity, or otherwise\nsafe to repeat. Contract mutations already have their own retry behavior and\nretry only `DISCONNECTED` by default.\n\n## Deduplication\n\n`deduplicate(options?)` from `@emdash/shared/requests` is handler middleware for\nsharing one in-flight execution for identical inputs:\n\n```ts\nconst expensiveStats = compose(rawExpensiveStats, [\n  deduplicate({ key: (input) => `${input.repo}:${input.branch}` }),\n]);\n```\n\nThe default key is stable JSON identity. Settled calls are not cached and\nrejections are not cached.\n\nCaller cancellation is independent from shared execution. One caller may stop\nwaiting without aborting the underlying request for other waiters. Set\n`cancelWhenUnused: true` only when it is correct to abort shared work after the\nfinal waiter leaves.\n\n## What Stays Out Of Middleware\n\nKeep protocol-visible behavior in contracts:\n\n- Endpoint kind.\n- Input and output schemas.\n- Upload size and MIME constraints.\n- Live model, log, stream, and job semantics.\n- Mutation idempotency envelopes.\n\nKeep UI presentation policy, such as command visibility or context keys, outside\nWire middleware. Keep domain failures in `Result` payloads when callers are\nexpected to handle them.\n","packages/wire/docs/api/serving.md":"# Serving and Clients\n\nThe API layer turns a contract into a server-side `Controller`, serves it over a\n`WireTransport`, and creates a typed client over the matching transport.\n\n## Creating a Controller\n\n`createController(contract, impl)` maps each endpoint to server behavior:\n\n```ts\nconst controller = createController(\n  notesApi,\n  {\n    session: sessionsHost,\n    activity: () => activityLogServer,\n    clearNotes: (input) => {\n      instance.states.notes.produce((draft) => {\n        draft.notes = [];\n      });\n      return instance.states.notes.snapshot().data;\n    },\n  }\n);\n```\n\nThe `impl` object is keyed by the contract shape. Procedures receive\n`(input, meta)`. Jobs use `{ run, toError? }`, a `LiveJobClientHandle`, or a\n`LiveJobReplicaCache`. Live logs use resolver functions, `LiveLogClientHandle`s, or\n`LiveLogReplicaCache`. Event streams use `createEventStreamHost()`, resolver functions,\nor `EventStreamClientHandle`s. Live model contracts use `expose()` or an existing\n`LiveModelClientHandle`.\n\nMutation idempotency is configured on `expose()`, not on the\ncontroller. The controller only routes calls, snapshots, attachments, and live\nmutation procedure envelopes.\n\nLive model state is separate from the contract because instances are runtime\nresources. A controller can be created once, while conversations, sessions, or\nwindows create and dispose keyed state through `family()` and scoped `expose()`\nlifecycles.\n\nSee the state bridge tests under `src/state/bridge/`.\n\n## Serving\n\n`serve(transport, controller, options?)` listens for protocol messages:\n\n- `call` invokes `controller.call(path, input, meta)`.\n- `snapshot` calls `LiveSource.snapshot()`.\n- `attach` subscribes to a live source and forwards `update` messages.\n- `detach` unsubscribes.\n- `cancel` aborts an in-flight call by id.\n\n```ts\nconst pair = memoryTransportPair();\nconst stop = serve(pair.right, controller, {\n  logger,\n  instrumentation: { callEnd: (event) => logger.debug('call finished', event) },\n});\n```\n\n`serve()` returns an unsubscribe. Call it when the transport or server session\ngoes away. It also aborts in-flight calls and detaches live subscriptions when\nthe transport disconnects or when the serve loop is disposed.\n\n## Validation\n\nContract validation is built into `createController()`. Every controller\nvalidates by default, and the default is environment-sensitive: `'full'`\n(inputs + outputs) everywhere except production, `'inputs'` in production\n(`NODE_ENV === 'production'`). Pass `validate` to override:\n\n```ts\nconst controller = createController(notesApi, impl, { validate: 'inputs' });\nconst stop = serve(pair.right, controller);\n```\n\nPolicies:\n\n- `none`: no schema parsing.\n- `inputs`: parse procedure inputs, upload/download inputs, live model mutation\n  envelopes, job start/cancel inputs, and live topic keys before delegating.\n- `full`: includes `inputs`, then also parses procedure outputs, upload/download\n  results, mutation results, and live job start results.\n\nThe default rule applies identically to bare controllers, component instances,\ncomponent workers, and tests (`createTestWire`), so a contract violation in a\ntest fails the same way it would in dev. Production boundaries stay on\n`inputs`: values crossing a trust boundary are always parsed, while output\nvalidation is skipped where it is too expensive.\n\nValidation covers request/response values that pass through\n`Controller.call()` and live topic keys passed to `Controller.resolveLive()`.\nLive job progress, result, error values, and event stream payloads are emitted\nlater as live updates and are not intercepted.\n\n## Connecting\n\n`connect(transport, options?)` creates a low-level `Connection`:\n\n```ts\nconst connection = connect(pair.left, { instrumentation });\n```\n\n`Connection` supports:\n\n- `call(path, input, { signal? })`.\n- `snapshot(topic)`.\n- `attach(topic, push, { signal?, onReattach? })`.\n- `onDisconnect(cb)`.\n- `dispose()` to release the logical connection without closing its transport.\n\nOn disconnect, pending calls reject with `WireError` code `DISCONNECTED`.\nExisting attachments are retained locally. If the transport exposes\n`onReconnect`, `connect()` re-issues active `attach` requests after the replacement\nlink is live and then calls each attachment's `onReattach` callback.\n\nReplicas use `onReattach` for live models, logs, and jobs to force a fresh\nsnapshot after reattach. Direct client-handle consumers can use the same callback when\nthey need to reseed UI state after reconnect.\n\nAn attachment signal owns establishment for only that caller. Aborting it before readiness cancels\nthe pending attach when no other caller still needs the topic, while shared callers remain attached.\nAfter `attach()` resolves, the returned unsubscribe owns the established interest.\n\n`dispose()` rejects pending calls, releases live attachments and blob channels, and\nunsubscribes the connection from transport events. It intentionally does not call\n`transport.close()`. This allows a short-lived application handshake connection to\nvalidate a candidate transport before handing that same transport to a stable\nreconnecting connection.\n\nThe protocol layer intentionally has no version handshake. Receivers validate the\nmessage `kind` and required fields in `isWireMessage()`; unknown message kinds are\nignored by transport adapters that parse untrusted frames.\n\n## Typed Clients\n\n`client(contract, connection)` returns a `ContractClient` with the same nested\nshape as the contract:\n\n```ts\nconst contractClient = client(notesApi, connection);\n\nconst sessions = createLiveModelReplicaCache(notesApi.session, contractClient.session, {\n  onChange: {\n    notes: (state, meta) => {\n      console.log('notes model:', state, meta.kind);\n    },\n  },\n});\nconst lease = sessions.acquire({ sessionId: 'demo' });\nconst session = await lease.ready();\n\nconst added = await session.mutations.addNote({ text: 'Typed client mutation' });\nawait added.settled;\nawait lease.release();\nawait sessions.dispose();\n```\n\nLive model and live log accessors are client handles. Use `state(key, name)` or\n`handle(key)` to snapshot/attach without local state, or pass the client handle\nto a replica wrapper.\n\nMutations return `ContractMutationInvocation`:\n\n```ts\ntype ContractMutationInvocation<D, E> = {\n  result: LiveMutationResult<D, E>;\n  settled: Promise<void>;\n};\n```\n\n`MutationCallOptions` lets callers provide a `mutationId`, a per-call\n`timeoutMs` deadline override, and an explicit opt-in retry policy. There is no\nautomatic retry; retries reuse the mutation id, so opted-in retries stay\nidempotent:\n\n```ts\nawait session.mutations.addNote({ text: 'Optimistic title' }, {\n  mutationId: 'custom-mutation',\n  retry: { schedule: retrySchedule({ delaysMs: [250, 1_000], maxRetries: 2 }) },\n});\n```\n\n## Cancellation\n\nWire supports cooperative cancellation for request messages. The client sends a\nprotocol message for the request id:\n\n```ts\n{ kind: 'cancel', id: callId }\n```\n\nTyped procedure clients accept an optional `{ signal }` argument:\n\n```ts\nconst abort = new AbortController();\nconst result = client.slowOperation({ id: 'task' }, { signal: abort.signal });\n\nabort.abort();\nawait result; // rejects with WireError code CANCELLED\n```\n\nIf the signal is already aborted, the call rejects locally without posting.\nServer procedure handlers receive the same signal through `CallMeta`:\n\n```ts\nconst controller = createController(api, {\n  slowOperation: async (input, meta) => {\n    await abortableWork(input, meta.signal);\n    return { ok: true };\n  },\n});\n```\n\nCancellation is cooperative. Long-running handlers should pass the signal into\ntheir own async work, listen for `abort`, or periodically check\n`meta.signal?.aborted`. `snapshot` and `attach` requests also share the same server-side\ncancellation registry. Resourced event-stream activation receives an `AbortSignal` so it can release\npartially-created resources when the final pending attachment disappears.\n\nMutations are intentionally not cancellable through this API; they use\n`mutationId` for idempotency and retry. See [mutations](../live/mutations.md).\nCancelling a `liveJob().start` wire call does not cancel the spawned job; job\ncancellation is domain-level through the generated `<path>.cancel` procedure.\n\n## Composing Controllers\n\nMiddle tiers compose by creating a typed client for the upstream contract and\npassing client handles, local handlers, or replicas into a new controller. This\nkeeps forwarding typed and makes the chosen interception points explicit:\n\nIf the middle tier should only relay the entire upstream contract, use\n`forwardController(contract, client)` instead of spelling out every endpoint:\n\n```ts\nconst upstream = client(workspaceApi, connect(upstreamTransport));\nconst controller = forwardController(workspaceApi, upstream);\n```\n\nUse explicit `createController()` composition when the hop needs local behavior:\n\n```ts\nconst upstreamConnection = connect(upstreamTransport);\nconst upstream = client(workspaceApi, upstreamConnection);\n\nconst conversations = createLiveModelReplicaCache(workspaceApi.conversation, upstream.conversation, {\n  lingerMs: 10 * 60_000,\n});\n\nconst controller = createController(workspaceApi, {\n  // Local override.\n  ping: async () => 'desktop-main',\n\n  // Stateless forwarding.\n  git: upstream.git,\n\n  // Stateful interception/cache before serving downstream clients.\n  conversation: conversations,\n});\n```\n\nFor nested contracts, object destructuring is enough to assemble the\nimplementation shape:\n\n```ts\nconst controller = createController(appApi, {\n  ...upstream,\n  settings: {\n    ...upstream.settings,\n    save: async (input, meta) => {\n      await auditSettingsChange(input, meta.signal);\n      return upstream.settings.save(input, meta);\n    },\n  },\n});\n```\n\nUse replicas when the middle tier needs to inspect, cache, or transform live\nstate before exposing it downstream. Use client handles directly when the middle\ntier is only forwarding calls and live sources.\n\n## Multi-Window Sessions\n\n`createWireSessionHub(controller)` serves the same controller to multiple\ntransport sessions:\n\n```ts\nconst hub = createWireSessionHub(controller);\nconst pair = memoryTransportPair();\n\nhub.open('window-1', pair.right);\nconst contractClient = client(api, connect(pair.left));\n```\n\nOpening the same session id closes the previous transport. `close(id)` closes\none session and calls `transport.close?.()` after disposing the serve loop.\n`dispose()` is async: it closes all sessions and awaits `controller.dispose?.()`.\n\n`Controller.dispose`, when present, returns `Promise<void>`. Wrappers such as\nvalidation, logging, session hubs, tests, process runtimes, and Electron exposure\npropagate that promise so LiveJobSource servers and resource hosts can finish shutdown\nbefore their owner reports disposed.\n\n## Server-Side Call Helpers\n\nFor the full middleware pattern, including handler middleware versus controller\nmiddleware, see [composable middleware](./middleware.md).\n\n`compose(target, middlewares)` from `@emdash/shared/requests` applies target-first\nmiddleware arrays to handlers or controllers. The first array entry is outermost:\nit sees the request first and settles last.\n\n```ts\nconst loadStats = compose(\n  async (input, meta) => {\n    return await fetchStats(input.repo, { signal: meta.signal });\n  },\n  [\n    withTimeout({ timeoutMs: 20_000 }),\n    withRetry({ schedule, shouldRetry: isTransient }),\n    withTimeout({ timeoutMs: 5_000 }),\n  ]\n);\n\nconst controller = createController(api, { loadStats });\n```\n\nIn this example the outer timeout bounds the complete retry program, while the\ninner timeout bounds each attempt. Handler middleware must preserve the handler's\nshape. Procedure handlers receive `(input, meta)` and middleware should preserve\nall `meta` fields while deriving a replacement `meta.signal` when it needs\ncooperative cancellation.\n\n`deduplicate(options?)` from `@emdash/shared/requests` wraps procedure\nimplementations to share one in-flight promise for identical inputs:\n\n```ts\nconst controller = createController(api, {\n  expensiveStats: compose(\n    async (input, meta) => {\n      return await loadStats(input.repo, input.branch, { signal: meta.signal });\n    },\n    [deduplicate({ key: (input) => `${input.repo}:${input.branch}` })]\n  ),\n});\n```\n\nBehavior:\n\n- Default key is `stableStringify(input)`, so object property order does not\n  matter.\n- Only in-flight calls are deduplicated. Settled calls are not cached.\n- Rejections are not cached.\n- `meta.signal` is not part of the key and shared execution is not aborted by\n  one caller.\n- Do not wrap mutations; mutation idempotency is handled by `mutationId`.\n\nSee [../../examples/dedupe/server.ts](../../examples/dedupe/server.ts).\n","packages/wire/docs/api/transports.md":"# Transports\n\nA `WireTransport` is the protocol boundary:\n\n```ts\ntype WireTransport = {\n  post(message: WireMessage): void;\n  onMessage(cb: (message: WireMessage) => void): Unsubscribe;\n  onDisconnect(cb: () => void): Unsubscribe;\n  onReconnect?(cb: () => void): Unsubscribe;\n  onTerminalFailure?(cb: (error: unknown) => void): Unsubscribe;\n  close?(): void;\n};\n```\n\nThe same `serve()`, `connect()`, and `client()` code works across every\ntransport. Only construction changes.\n\n`onReconnect` is optional and fires whenever the transport establishes\nconnectivity, including the first time. Its presence marks the transport as\nreconnect-capable: `connect()` then holds calls issued while disconnected until\nthe next reconnect or the call deadline, and re-attaches live topics after each\nreconnect; replicas then force a fresh snapshot. Transports without it fail\ncalls immediately while disconnected. `onTerminalFailure` fires once when the\ntransport gives up permanently; `connect()` rejects all held and future calls\nwith a `WireError('DISCONNECTED')` carrying the cause. `close()` releases\nlisteners registered by the adapter. It closes the underlying channel only when\nthe adapter owns a closeable channel, such as a `MessagePort`.\n\n## Memory\n\n`memoryTransportPair()` creates paired in-process transports for tests and\nexamples:\n\n```ts\nconst pair = memoryTransportPair();\nserve(pair.right, controller);\n\nconst contractClient = client(api, connect(pair.left));\n```\n\n`pair.disconnect()` disconnects both sides. Each endpoint also exposes `close()`,\nwhich aliases the pair disconnect for test cleanup.\n\n`left` and `right` are the two ends of one duplex channel, not semantic roles.\nPosting on `left` delivers to listeners registered on `right`, and posting on\n`right` delivers to listeners registered on `left`. This mirrors\n`MessageChannel`'s `port1`/`port2`: tests commonly call `serve(pair.right, ...)`\nand `connect(pair.left)`, but the opposite assignment is equally valid.\n\n## DOM MessagePort\n\n`domPortTransport(port)` adapts browser `MessagePort` objects:\n\n```ts\nconst channel = new MessageChannel();\nserve(domPortTransport(channel.port1), controller);\n\nconst connection = connect(domPortTransport(channel.port2));\n```\n\nThe adapter calls `port.start?.()` and listens for `message` and `close` events.\n`close()` removes those listeners and calls `port.close?.()`.\n\n## Electron Windows\n\n`exposeWireToWindows()` serves one controller to many Electron renderer windows\nusing `MessageChannelMain`-style ports:\n\n```ts\nconst stop = exposeWireToWindows(\n  {\n    ipcMain,\n    createMessageChannel: () => new MessageChannelMain(),\n  },\n  controller,\n  { channel: 'wire' }\n);\n```\n\nThe renderer asks for a port, then waits for the browser-side transfer:\n\n```ts\nawait requestWirePort({ ipcRenderer, window }, { channel: 'wire' });\nconst port = await awaitWirePort(window, { channel: 'wire' });\nconst contractClient = client(api, connect(domPortTransport(port as MessagePort)));\n```\n\nOpening a new port for the same `webContents.id` closes the old one. Naturally\nclosed ports are removed from the session map. Internally, the helper uses\n`createWireSessionHub(controller)`, so session teardown also closes the transport.\n\n## Node Streams\n\n`streamTransport(input, output)` frames messages as newline-delimited JSON. It is\nuseful for subprocess, stdio, and SSH-style boundaries:\n\n```ts\nconst transport = streamTransport(child.stdout, child.stdin);\nconst contractClient = client(api, connect(transport));\n```\n\nMalformed frames are ignored. `close`, `end`, and `error` on the readable side\ntrigger disconnect listeners. `close()` stops parsing and clears local listeners;\nit does not close the readable or writable streams because those streams are owned\nby the caller.\n\n## Reconnecting\n\n`reconnectingTransport(connectOnce, options?)` wraps an async transport factory:\n\n```ts\nconst transport = reconnectingTransport(\n  async () => {\n    const pair = await openRemoteWirePair();\n    return pair.left;\n  },\n  { backoffMs: [100, 250, 500, 1000] }\n);\n\nawait transport.ready();\n```\n\n`connectOnce` is the readiness boundary for one physical connection. The returned\ntransport remains private until the promise resolves. Applications that require a\nhandshake should open the candidate, run the handshake against it, dispose the\ntemporary logical `Connection`, and only then return the candidate:\n\n```ts\nconst transport = reconnectingTransport(\n  async () => {\n    const candidate = await openRemoteTransport();\n    const handshakeConnection = connect(candidate);\n    const handshakeClient = client(api, handshakeConnection);\n    try {\n      const result = await handshakeClient.initialize({ version: PROTOCOL_VERSION });\n      if (!result.success) throw new ProtocolMismatchError(result.error);\n      return candidate;\n    } catch (error) {\n      candidate.close?.();\n      throw error;\n    } finally {\n      handshakeConnection.dispose();\n    }\n  },\n  {\n    shouldRetry: (error) => !(error instanceof ProtocolMismatchError),\n  }\n);\n\nawait transport.ready();\nconst stableClient = client(api, connect(transport));\n```\n\nThis ordering guarantees that held calls and live-topic reattachments cannot\novertake an application handshake. Wire itself remains version-agnostic; the\napplication owns the handshake contract and permanent-error classification.\n\nThe reconnecting transport does not queue messages. `post()` while no ready\ninner transport exists throws a `WireError('DISCONNECTED')`; robustness is\nowned entirely by `connect()`, which holds calls issued while disconnected\nuntil the next reconnect or the call deadline. When an inner transport\ndisconnects, listeners are notified, readiness resets, and reconnection starts.\n\n`ready()` resolves after the current `connectOnce` succeeds and its candidate is\ninstalled. After a disconnect, a new call to `ready()` waits for the replacement\ngeneration. A previously resolved readiness promise does not become pending\nagain, so disconnect observers should call `ready()` after receiving the\ndisconnect event.\n\n`shouldRetry(error, { attempt, isReconnect })` classifies factory or handshake\nfailures. Returning `false`, or exhausting a finite `retrySchedule`, permanently\nstops that transport, rejects its current readiness promise, and fires\n`onTerminalFailure` with the terminal error. Future `post()` calls fail with the\nterminal error. By default, failures remain retryable under the configured\nschedule.\n\n`onReconnect` fires whenever an inner transport reaches readiness, including\nthe first one. `Connection` listens for that signal to flush held calls and\nre-issue active `attach` requests; replicas refresh their snapshots after\nreattach. `close()` stops the retry loop, closes the current inner transport if\npresent, fires `onTerminalFailure`, and clears listeners.\n\n## Composition Notes\n\n`reconnectingTransport()` accepts `clock` and `retrySchedule` options. The legacy\n`backoffMs` array is normalized to a repeat-last retry schedule. Closing the\ntransport synchronously aborts pending reconnect sleeps and disposes late\nconnections.\n\nElectron window exposure returns an async cleanup function. Await it when tearing\ndown a runtime host so session hubs and controllers finish disposal before the\nworker or window bridge is replaced.\n\nTransport composition is ordinary function wrapping, so order matters when\nstacking adapters around `reconnectingTransport()`. For semantic telemetry (one\nevent per logical call, snapshot, attachment, cancellation, resync, or mutation\ndedupe), attach a `WireInstrumentation`; see\n[observability](../observability.md).\n","packages/wire/docs/live/event-stream.md":"# Event Streams\n\n`eventStream({ key, event })` exposes a keyed server-to-client event channel for\nloss-tolerant notifications. It reuses the wire live attachment protocol but does\nnot retain events or expose meaningful snapshots.\n\nUse event streams when subscribers can recover from missed events by resyncing\nfrom another source of truth, such as a file tree, cache, or database query. Use\n`liveLog` for retained append-only text and `liveModel` for convergent state.\n\n## Contract\n\n```ts\nconst api = defineContract({\n  fileEvents: eventStream({\n    key: z.object({ rootPath: z.string() }),\n    event: z.object({\n      kind: z.enum(['create', 'update', 'delete']),\n      path: z.string(),\n    }),\n  }),\n});\n```\n\nThe key addresses an event stream instance. The event schema types the\nclient-side `onEvent` callback and documents the payload shape.\n\n## Server\n\nMost servers use `createEventStreamHost()`:\n\n```ts\nconst fileEvents = createEventStreamHost(api.fileEvents);\n\nconst controller = createController(api, {\n  fileEvents,\n});\n\nfileEvents.emit(\n  { rootPath: '/repo' },\n  { kind: 'update', path: '/repo/package.json' }\n);\n```\n\n`emit()` is fire-and-forget. If no client is attached to that key, the event is\ndropped. When the last subscriber detaches, the host discards the keyed source.\n\nIf attachment presence owns an external resource, pass lifecycle hooks to the\nhost. `onActive` runs when the first subscriber attaches to a key, and `onIdle`\nruns when the last subscriber detaches:\n\n```ts\nconst resourceEvents = resourcedStream({\n  key: z.object({ rootPath: z.string() }),\n  event: z.object({ kind: z.string(), path: z.string() }),\n});\nconst fileEvents = createEventStreamHost(resourceEvents, {\n  onActive(key) {\n    void startWatcher(key);\n  },\n  onIdle(key) {\n    void stopWatcher(key);\n  },\n});\n```\n\nThis is useful for resources that should be recreated after reconnect. The wire\nclient automatically reattaches live topics after reconnect, so `onActive` runs\nagain in the new server process.\n\nWhen a successful attach must mean that the resource is ready, define the endpoint with\n`resourcedStream()` and provide `activate`. Activation receives a signal and resolves to its owned\ndisposer:\n\n```ts\nconst fileEvents = createEventStreamHost(api.fileEvents, {\n  async activate(key, signal) {\n    const watcher = await startWatcher(key, { signal });\n    return () => watcher.dispose();\n  },\n});\n```\n\nConcurrent subscribers share one activation. The host aborts pending activation when the final\nsubscriber leaves, disposes a late result, and keeps activation alive when another subscriber still\nowns the key.\n\nYou can also pass a resolver to `createController()` if another component owns\nthe source:\n\n```ts\ncreateController(api, {\n  fileEvents: (key) => sources.get(key.rootPath),\n});\n```\n\n## Client\n\n```ts\nconst unsubscribe = await client.fileEvents.subscribe(\n  { rootPath: '/repo' },\n  {\n    onEvent(event) {\n      console.log(event.kind, event.path);\n    },\n    onGap() {\n      void reloadFileTree();\n    },\n    onError(error, { retrying }) {\n      if (!retrying) showFileEventStreamError(error.message);\n    },\n    signal,\n  }\n);\n```\n\n`onGap` runs after the underlying transport reattaches. Events may have been lost\nwhile the link was disconnected or while a supervised subprocess restarted, so\nconsumers should refresh any derived state in that callback.\n\nInitial attachment does not call `onGap`; subscribers should perform their own\ninitial load before or after subscribing.\n\nThe `subscribe()` promise rejects if the initial attachment fails. After an\nattachment is established, `DISCONNECTED` reattach failures call `onError` with\n`retrying: true` and remain registered for a later reconnect. Non-retryable\nreattach errors call `onError` with `retrying: false` and terminate the\nattachment.\n\n## Semantics\n\n- Events are at-most-once and only delivered to currently attached clients.\n- No historical events are retained for late subscribers.\n- Event stream keys are validated by controller validation.\n- Event payloads are not validated on the push path, matching other live updates.\n- Forwarding preserves upstream gaps and reattach errors even when the downstream\n  transport remains connected.\n\n## Example\n\nSee [../../examples/event-stream/client.ts](../../examples/event-stream/client.ts)\nfor a runnable in-process example.\n","packages/wire/docs/live/live-job.md":"# Live Jobs\n\nLive jobs model async work as a live state machine. They are useful when callers\nneed both a final result and a stream of progress updates.\n\n## State Shape\n\n`liveJobStateSchema(progress, result, error)` creates a schema for:\n\n```ts\ntype LiveJobState<P, R, E> =\n  | { status: 'running'; startedAt: number; progress: P[]; progressCount: number }\n  | { status: 'succeeded'; startedAt: number; finishedAt: number; progress: P[]; result: R }\n  | { status: 'failed'; startedAt: number; finishedAt: number; progress: P[]; error?: E; cause?: SerializedError }\n  | { status: 'cancelled'; startedAt: number; finishedAt: number; progress: P[] };\n```\n\n`progress` is a retained ring of recent entries. `progressCount` is the total\nnumber of progress events emitted, including entries that may have rolled out of\nthe retained ring. Terminal states retain lifecycle timestamps and the most\nrecent progress entries, but they do not carry `progressCount`.\n\n## Server\n\n`LiveJobSource<I, P, R, E>` accepts a handler that returns `Result<R, E>`:\n\n```ts\nconst jobs = new LiveJobSource<Input, Progress, Result, ErrorState>(\n  async (input, ctx) => {\n    ctx.progress({ step: 'checkout' });\n    await build(input, ctx.signal);\n    ctx.progress({ step: 'package' });\n    return ok({ artifact: `${input.target}.zip` });\n  },\n  { toError: (error) => ({ message: error instanceof Error ? error.message : String(error) }) }\n);\n\nconst { jobId } = jobs.start({ target: 'desktop' });\n```\n\nOptions:\n\n- `generation`: optional fixed generation for every job state model.\n- `terminalRetainMs`: how long terminal state remains attachable. Default:\n  `LIVE_JOB_TERMINAL_RETAIN_MS` (5 minutes).\n- `idFactory`: custom job id factory, useful for deterministic tests.\n- `clock`: custom timestamp source, useful for deterministic tests.\n- `toError`: optional fallback for unexpected thrown errors. Domain failures\n  should be returned as `err(error)` from the handler.\n- `onRunStarted`, `onRunChanged`, and `onRunEvicted`: optional hooks for callers\n  that want to maintain their own job listing.\n\nHandlers receive `LiveJobContext`:\n\n- `ctx.progress(value)` appends retained progress and emits an update.\n- `ctx.signal` is an `AbortSignal`. If the signal is aborted, the job finishes\n  as `cancelled`.\n\nReturn `ok(result)` for successful completion and `err(error)` for expected\ndomain failures. If the handler throws and `toError` is configured, the thrown\nvalue is mapped into typed `error`; otherwise the failed state carries a\nserialized `cause`.\n\n`source(jobId)` returns a read-only live source for that job. `snapshot(jobId)` and\n`getState(jobId)` are convenience accessors. `cancel(id)` aborts a running job.\n`dispose()` is async: it aborts running jobs, waits for cooperative handlers to\nsettle, clears eviction timers, and removes all job state.\n\nTerminal job state is retained for `LIVE_JOB_TERMINAL_RETAIN_MS` (5 minutes) so\nlate clients can reattach by id. This retention is process-local, not durable.\n\n## Consumers\n\nConsumers normally reach jobs through the API layer. The contract client starts,\ncancels, and attaches to jobs without retaining local state:\n\n```ts\nconst { jobId } = await contractClient.build.start({ target: 'desktop' });\nconst handle = contractClient.build.handle(jobId);\n\nconst detach = await handle.attach((update) => {\n  console.log(update.delta);\n});\n\nawait contractClient.build.cancel(jobId);\ndetach();\n```\n\nUse `createLiveJobReplicaCache()` when a consumer wants a `result` promise, progress\ncallbacks, ref-counted attachment, and terminal-state retention. If the job fails,\n`ReplicaJob.result` rejects with `LiveJobFailedError`; if it is cancelled, it\nrejects with `LiveJobCancelledError`. Progress emitted by a seed is suppressed so\nreattaching clients do not replay old progress as fresh events.\n\n## Contract Endpoint\n\nThe API contract layer can expose a job directly:\n\n```ts\nconst api = defineContract({\n  build: liveJob({\n    input: z.object({ target: z.string() }),\n    progress: z.object({ step: z.string() }),\n    result: z.object({ artifact: z.string() }),\n    error: z.object({ message: z.string() }),\n  }),\n});\n```\n\nOn the server, implement the endpoint with `{ run, toError? }`:\n\n```ts\nconst controller = createController(api, {\n  build: {\n    run: async (input, ctx) => {\n      ctx.progress({ step: 'compile' });\n      return ok({ artifact: `${input.target}.zip` });\n    },\n    toError: (error) => ({\n      message: error instanceof Error ? error.message : String(error),\n    }),\n  },\n});\n```\n\nThe typed client exposes a live job client handle. Use it directly for\nlow-level forwarding, or wrap it in `createLiveJobReplicaCache()` when a consumer wants\nref-counted local state:\n\n```ts\nconst jobs = createLiveJobReplicaCache(api.build, contractClient.build, { lingerMs: 30_000 });\nconst lease = await jobs.start({ target: 'desktop' });\nconst handle = await lease.ready();\n\nhandle.onProgress((progress) => console.log(progress.step));\nconsole.log(await handle.result);\n\nawait lease.release();\n\nconst reattachedLease = jobs.acquire(handle.jobId);\nconst reattached = await reattachedLease.ready();\nconsole.log(await reattached.result);\nawait reattachedLease.release();\nawait jobs.dispose();\n```\n\n`ReplicaJob` exposes `jobId`, `ready`, `result`, `getState()`,\n`onProgress(cb)`, and `cancel()`. The `LiveJobReplicaCache` manager owns ref counting,\nsubscription sharing, and terminal-state retention.\n\n`LiveJobSource` accepts a `Clock` for deterministic start/finish timestamps and\nterminal retention eviction. Server disposal is awaitable: it cancels active job\nruns, waits for cooperative handlers to settle, and disposes retention timers\nbefore returning.\n\nCancellation at the procedure/wire level is documented in\n[serving](../api/serving.md#cancellation). Job cancellation is domain-level:\n`ReplicaJob.cancel()` calls the generated `<path>.cancel` procedure, which calls\n`LiveJobSource.cancel(jobId)`.\n\nSee [../../examples/job-contract/client.ts](../../examples/job-contract/client.ts).\n","packages/wire/docs/live/live-log.md":"# Live Logs\n\nLive logs are append-only text streams with retained tail snapshots. They are a\ngood fit for terminal output, build logs, and agent process output where clients\nneed to attach late and still see recent context.\n\n## Server\n\n`LiveLogSource` stores a bounded text buffer. `append(chunk)` emits\n`{ chunk }` deltas to subscribers and updates the retained snapshot:\n\n```ts\nconst server = new LiveLogSource({ generation: 3000, maxBufferBytes: 12 });\n\nexport function appendLine(line: string): void {\n  server.append(`${line}\\n`);\n}\n```\n\nOptions:\n\n- `generation`: optional fixed generation, useful in tests.\n- `maxBufferBytes`: retained buffer size. The default is 1 MiB.\n\nSnapshots contain `LiveLogSnapshotData`:\n\n```ts\ntype LiveLogSnapshotData = {\n  baseOffset: number;\n  text: string;\n  truncated: boolean;\n};\n```\n\n`baseOffset` is the byte offset of `text` in the logical full log. When the\nserver drops old bytes because `maxBufferBytes` was exceeded, `truncated` is\n`true`. The server keeps at least the newest chunk even if it exceeds the byte\nlimit.\n\n`reseed()` starts a new generation and resets sequence to `0`. With no argument,\nit clears retained text and resets `baseOffset` to `0`; with retained snapshot\ndata, it seeds the buffer from that data.\n\n## Consumers\n\nConsumers normally reach logs through the API layer. The client handle is useful\nwhen you want to stream directly into a terminal or log viewer:\n\n```ts\nconst output = contractClient.activity.handle(session);\nconst snapshot = await output.snapshot();\nterm.write(snapshot.data.text);\n\nconst detach = await output.attach((update) => {\n  term.write((update.delta as { chunk: string }).chunk);\n});\n```\n\nUse `createLiveLogReplicaCache()` when a process wants a retained local buffer that can\nalso be served downstream. Without a custom sink, the replica keeps an eager\nbounded `LiveLogSource` buffer and `text()` remains readable. With a custom `LogSink`,\nthe sink owns storage; readable `LogStore`s add `text()`, while write-only sinks\ncan stream directly into xterm without a duplicate text buffer.\n\nThe replica's internal log follower tracks the followed generation/sequence\nplus the UTF-8 byte offset already written to its sink. On reconnect or explicit `refresh()`, it\nsplices from the retained snapshot when the generation still matches, appending\nonly missing bytes instead of resetting rendered output. Generation changes or\nevicted gaps still reset the sink from the retained snapshot.\n\nThe underlying protocol follower resyncs on update-before-seed, generation\nmismatch, sequence gap, or invalid log delta. Resync events use the same `resync`\ninstrumentation hook as live models.\n\n## API Layer Usage\n\nContracts expose logs with `liveLog({ key })`:\n\n```ts\nconst api = defineContract({\n  activity: liveLog({ key: sessionKeySchema }),\n});\n\nconst controller = createController(api, {\n  activity: () => activityLogServer,\n});\n\nconst activity = contractClient.activity.handle(session);\nconst detach = await activity.attach((update) => {\n  console.log((update.delta as { chunk: string }).chunk);\n});\n\ndetach();\n```\n\nThe API layer handles topic encoding, snapshots, attachment, and detachment.\n\nSee [../../examples/live-log/client.ts](../../examples/live-log/client.ts).\n","packages/wire/docs/live/live-model.md":"# Live States and Protocol\n\nLive states are reactive JSON state containers. A server owns authoritative\nstate, emits ordered Immer patches, and clients apply those patches locally.\nWhen a client detects a gap, it refetches a snapshot and resumes from the new\ngeneration.\n\n## Protocol Terms\n\nThe shared protocol lives in `src/live/protocol`.\n\n- `LiveSnapshot<T>` is the full state at a cursor:\n  `{ generation, sequence, timestamp, data }`.\n- `LiveUpdate` is an ordered delta:\n  `{ generation, baseSequence, sequence, timestamp, delta, mutationIds? }`.\n- `LiveCursor` is `{ generation, sequence }` and identifies a point in a live\n  state stream.\n- `LiveSource` is the common server-side shape consumed by the API layer:\n  `snapshot()` plus `subscribe(cb)`.\n\n`generation` changes when the server calls `reseed()`. `sequence` increments for\neach effective patch in a generation. `baseSequence` must equal the client's\ncurrent sequence; otherwise the client has missed an update and resyncs from\n`snapshot()`.\n\n## Server\n\n`LiveStateSource<T>` owns one authoritative state object. Mutate it with\n`produce()`, not by mutating the original object:\n\n```ts\nconst server = new LiveStateSource<TaskListState>(\n  {\n    tasks: [{ id: 'task-1', title: 'Read the plan', done: false }],\n    filter: 'all',\n  },\n  1000\n);\n\nconst cursor = server.produce(\n  (draft) => {\n    draft.tasks.push({ id: 'task-2', title: 'Apply the first patch', done: false });\n  },\n  { mutationIds: ['example-add-task'] }\n);\n```\n\n`produce()` returns the cursor containing the change. If the mutator is a no-op,\nno update is emitted and the current cursor is returned. `snapshot()` deep\nclones the current state. `reseed(next?)` replaces the generation, resets\nsequence to `0`, optionally replaces state, and forces clients to resync on the\nnext observed update.\n\n## Consumers\n\nConsumers normally reach live states through a live model contract. Use the\ncontract client when you only need protocol access:\n\n```ts\nconst model = contractClient.conversation.state({ sessionId }, 'state');\nconst snapshot = await model.snapshot();\nconst detach = await model.attach((update) => {\n  console.log(update.delta);\n});\n```\n\nWrap that live state client handle in `createLiveModelReplicaCache()` when a process wants local\nstate, mutation settling, ref counting, or a downstream `LiveSource`; see\n[replicas](./replicas.md#model-replicas).\n\nThe package also has low-level protocol followers internally. They consume\nsnapshots and ordered updates, then resync when:\n\n- an update arrives before `seed()`.\n- `generation` differs from the local generation.\n- `baseSequence` differs from the local sequence.\n- Immer patch application throws.\n- schema validation fails in non-production builds.\n\nThe reported resync reasons are `generation`, `sequence-gap`, `patch-failed`,\nand `validation`. Schema validation is skipped when `NODE_ENV === 'production'`;\nthe generation and sequence checks are the primary correctness mechanism.\n\nEvery follower is constructed with a required resync failure policy that\ndecides what happens when the snapshot refetch itself fails. Wire ships two\ncanned policies: `resyncRetry({ schedule? })` retries on a bounded backoff\nuntil success or dispose, and `resyncMarkStale()` gives up while keeping\nstaleness observable (`stale` on the client) until the next update or reattach\ntriggers a fresh attempt. Production replicas use `resyncRetry`. Every failure\nfires the `resyncFailed` instrumentation event. `refresh()` resolves only when\nthe follower is fresh: concurrent calls coalesce onto the in-flight resync and\na trigger arriving mid-reseed re-runs the loop before resolution. Disposing a\nfollower-based client rejects all of its pending waiters — including\nwait-forever waiters (`timeoutMs <= 0`) — with a disposed error.\n\n## Cursor and Mutation Waiters\n\n`waitForCursor(cursor, timeoutMs?)` resolves when the client has reached a\ncursor. `waitForMutation(mutationId, timeoutMs?)` resolves when an applied update\ncontains the mutation id, or when a seed/resync lands because the snapshot is\nauthoritative.\n\nThese waiters are used by mutation settling; see [mutations](./mutations.md).\n\nSee [../../examples/state-kernel/client.ts](../../examples/state-kernel/client.ts)\nfor an end-to-end example.\n","packages/wire/docs/live/mutations.md":"# Mutations\n\nMutations connect API calls to live model updates. A mutation can update one\nmodel, many instances of one model, or several model refs. The key extra piece\nis the `mutationId`: it tags emitted `LiveUpdate`s so clients can prove their\nbound live models have observed the mutation.\n\n## Why Mutation IDs Exist\n\nAn RPC result only tells the caller that the server handler finished. It does\nnot prove every live subscription in the UI has applied the corresponding\npatches. Mutation ids bridge that gap:\n\n```ts\nserver.produce(\n  (draft) => {\n    draft.tasks.push({ id: 'task-2', title: 'Apply the first patch', done: false });\n  },\n  { mutationIds: ['example-add-task'] }\n);\n```\n\nThe update carries `mutationIds: ['example-add-task']`. A replica can resolve\n`waitForMutation('example-add-task')` when its local model applies that update.\n\n## Providers and Context\n\nLive model contract mutations run against a `LiveModelProvider` — most commonly\none produced by `expose()` from `@emdash/wire/state`, which bridges kernel state\nonto the endpoint. Mutation handlers update kernel cells and await the observed\nrevision so the returned cursor is settled:\n\n```ts\nconst sessions = expose(api.session, { metadata: (key) => metadataCell(key) }, {\n  mutations: {\n    async setTitle(ctx) {\n      const revision = metadataCell(ctx.key).update(\n        (previous) => ({ ...previous, title: ctx.input.title }),\n        { mutationIds: [ctx.mutationId] }\n      );\n      await ctx.observed('metadata', revision);\n      return ok({ title: ctx.input.title });\n    },\n  },\n});\n```\n\nKeys use `stableStringify()`, so object key order does not matter when providers\nand client bindings look up an instance. The mutation context is instance-bound:\n`ctx.key` identifies the addressed member and `ctx.observed(name, revision)`\nrecords the cursor of every touched member state. The wire result is:\n\n```ts\ntype LiveMutationResult<D, E> =\n  | { success: true; data: { data: D; cursors: LiveCursorEntry[] } }\n  | { success: false; error: E };\n```\n\nThe `data.data` value is the domain result. `data.cursors` tells the client which\nlive model bindings need to catch up.\n\n## Client Settling\n\n`LiveModelReplicaCache.acquire(key)` returns a `ReplicaInstance` for one group key. Its\nmutation methods call the live model client handle and then settle against the local\n`ReplicaState`s.\n\nGroup mutation methods return `{ result, settled }`:\n\n```ts\nconst sessions = createLiveModelReplicaCache(api.session, contractClient.session);\nconst lease = sessions.acquire({ sessionId: 'demo' });\nconst session = await lease.ready();\n\nconst added = await session.mutations.addNote({ text: 'Typed client mutation' });\nawait added.settled;\nawait lease.release();\nawait sessions.dispose();\n```\n\n`settled` waits for every cursor in the mutation result. For each cursor entry,\nit resolves when either:\n\n- the matching binding applies an update tagged with the mutation id, or\n- the matching binding reaches the returned cursor.\n\nThis lets UI code safely read live client snapshots after `await settled`.\n\n## Group Contract Mutations\n\nThe API layer integrates mutations through `liveModel()` member\nmutations. Each group mutation becomes a client method:\n\n```ts\nconst updated = await session.mutations.setTitle({ title: 'Grouped wire' }, {\n  mutationId: 'custom-mutation',\n});\nawait updated.settled;\n```\n\nIf no id is provided, the replica mutation helper generates one with\n`createMutationId()`.\nExplicit ids are useful for optimistic previews, where the preview and server\nmutation must share the same confirmation id.\n\n## Idempotency and Retries\n\nThe server-side idempotency cache used by `expose()` stores settled mutation\nresults by `mutationId` and shares one in-flight execution for concurrent\nduplicates.\n\nBy default the cache keeps entries for 5 minutes with a 1000-entry cap.\nConfigure or disable it through the `idempotency` option:\n\n```ts\nconst sessions = expose(api.session, states, {\n  mutations,\n  idempotency: { ttlMs: 60_000, maxEntries: 500 },\n});\n\nconst withoutDedupe = expose(api.session, states, { mutations, idempotency: false });\n```\n\nThe client never retries mutations automatically. Callers opt in per call with\nan explicit retry schedule; retries reuse the same `mutationId`, so the\nserver-side cache deduplicates them:\n\n```ts\nawait session.addNote(input, {\n  mutationId: 'add-note-1',\n  retry: { schedule: retrySchedule({ delaysMs: [250, 1_000], maxRetries: 2 }) },\n});\n```\n\nOpted-in retries fire only for `DISCONNECTED` and `TIMEOUT` errors; they never\nhappen for `CANCELLED` errors.\n\nThe cache is process-local and temporary. It provides at-most-once behavior\nwithin one server process lifetime, not durable exactly-once semantics. If a\nmutation has durable side effects such as database writes, store the\n`mutationId` in that domain layer too.\n\nUse `procedure()` for API calls that do not need live model cursor settling.\n`mutation()` is only valid as a member of `liveModel().mutations`\nin the contract API.\n\n","packages/wire/docs/live/replicas.md":"# Replicas\n\nReplicas are consumer-instantiated materialization wrappers around client handles from\n`client()`. The typed client itself never stores live state. A consumer chooses\none of four shapes:\n\n- Own state: `LiveStateSource`, `LiveLogSource`, `LiveJobSource`, or an\n  `expose()` provider over kernel state.\n- Stream directly: use a client `snapshot()`/`attach()` handle with no local store,\n  for example PTY output written straight into xterm.\n- Forward: pass client handles or subtrees to `createController()` so a hop stays stateless.\n- Replica: wrap a client handle to hold local state, share upstream subscriptions, and\n  serve downstream clients.\n\nEvery replica manager has the same lifecycle shape:\n\n```ts\nconst lease = replica.acquire(key);\nconst instance = await lease.ready();\nawait lease.release();\nawait replica.dispose();\n```\n\n`acquire()` increments the ref count for that key. The first lease opens the\nupstream subscription, concurrent leases share it, and the last release starts\nthe optional `lingerMs` timer. `peek(key)` returns a warm instance while it is\nretained.\n\n## Model Replicas\n\nLive models are exposed only through `liveModel()`. A\n`LiveModelReplicaCache` follows a live model client handle and yields a `ReplicaInstance`:\n\n```ts\nimport { createImmutableMobxStore } from '@emdash/wire/mobx';\n\nconst conversations = createLiveModelReplicaCache(api.conversation, contractClient.conversation, {\n  lingerMs: 30_000,\n  stores: {\n    state: () => createImmutableMobxStore(),\n  },\n  onChange: {\n    state: (value, meta) => console.log(value, meta.kind),\n  },\n});\n\nconst lease = conversations.acquire({ conversationId: 'demo' });\nconst conversation = await lease.ready();\n\nconst updated = await conversation.mutations.setTitle({ title: 'Replicated' });\nawait updated.settled;\nconsole.log(conversation.states.state.current());\n```\n\n`ReplicaInstance.states` contains one `ReplicaState` per contract member.\n`ReplicaState` follows upstream snapshots and updates, stores current state in a\npluggable `StateStore`, and re-emits updates in a replica-local cursor space. If\nno store is supplied, the replica uses the plain immutable store.\nMobX consumers can pass stores per liveState through `stores`. Use\n`createImmutableMobxStore()` for snapshot/reference consumers such as\n`useSyncExternalStore`, or `createReactiveMobxStore()` for `observer` components\nthat benefit from property-level MobX tracking.\n\nMutation helpers return `{ result, settled }`. On success, the replica translates\nupstream cursors to local cursors before returning them to downstream clients, so\nboth local UI and served clients settle against the same local state.\n\nA cursor-translation timeout never fails a committed mutation. If the replica\ncannot observe the mutation's upstream cursor in time, the mutation still\nresolves successfully and the result data carries `settled: false` (client-side\nmetadata; the wire shape of `LiveMutationResult` is unchanged). Instrumentation\nrecords the timeout via `cursorTranslationTimeout`, and the `settled` promise\nstays best-effort as before.\n\n## Log Replicas\n\nUse a `LiveLogReplicaCache` when a process needs a local retained text buffer or wants\nto serve log output downstream:\n\n```ts\nimport { createMobxLogStore } from '@emdash/wire/mobx';\n\nconst outputs = createLiveLogReplicaCache(api.ptyOutput, contractClient.ptyOutput, {\n  lingerMs: 10_000,\n  maxBufferBytes: 1024 * 1024,\n  store: () => createMobxLogStore(),\n});\n\nconst lease = outputs.acquire({ sessionId });\nconst output = await lease.ready();\noutput.onAppend((chunk) => index(chunk));\nconsole.log(output.text());\n```\n\nWithout a custom sink, the log replica keeps an eager internal `LiveLogSource` buffer\nthat is readable through `text()` and can serve downstream clients. With a custom\nsink, the sink owns storage: readable `LogStore`s expose `text()`, while\nwrite-only `LogSink`s (for example xterm scrollback) support reset/append without\nallocating a duplicate string buffer.\n\nFor terminal rendering, prefer the client handle directly:\n\n```ts\nconst output = contractClient.ptyOutput.handle({ sessionId });\nterm.write((await output.snapshot()).data.text);\nconst detach = await output.attach((update) => {\n  term.write((update.delta as { chunk: string }).chunk);\n});\n```\n\n## Job Replicas\n\n`LiveJobReplicaCache` wraps a live job client handle. It forwards `start()` and `cancel()`,\nmaterializes job state by `jobId`, and keeps terminal state readable under lease\nor retention:\n\n```ts\nimport { createImmutableMobxStore } from '@emdash/wire/mobx';\n\nconst jobs = createLiveJobReplicaCache(api.build, contractClient.build, {\n  lingerMs: 30_000,\n  store: () => createImmutableMobxStore(),\n});\n\nconst lease = await jobs.start({ target: 'desktop' });\nconst job = await lease.ready();\njob.onProgress((progress) => console.log(progress.step));\nconsole.log(await job.result);\n\nawait lease.release();\n\nconst late = jobs.acquire(job.jobId);\nconsole.log((await late.ready()).getState());\n```\n\nWhen a job reaches `succeeded`, `failed`, or `cancelled`, the replica detaches\nfrom the upstream live topic but retains the local terminal state while leases or\n`lingerMs` keep it warm.\nThe job replica also supports a pluggable reset/current `JobStore`, so\n`getState()` can be observable. The internal `LiveStateSource` used for downstream\nserving is created only if `snapshot()` or `subscribe()` is requested.\n\n## Serving Replicas\n\nReplicas can be passed directly into `createController()`:\n\n```ts\nconst upstream = client(api, connect(sshTransport));\n\nconst controller = createController(api, {\n  conversation: createLiveModelReplicaCache(api.conversation, upstream.conversation),\n  ptyOutput: upstream.ptyOutput, // forward bytes without buffering\n  build: createLiveJobReplicaCache(api.build, upstream.build),\n});\n```\n\nUse replicas only when the hop needs local state. If the hop is a pure relay,\nforward the client subtree instead.\n","packages/wire/docs/observability.md":"# Observability\n\n`@emdash/wire` exposes observability at the same boundaries that own runtime\nbehavior: server calls, client calls, live topic attachment, mutation dedupe,\nlive-client resyncs, transport messages, and scope cleanup.\n\n## Ambient Logger\n\nThe logger interface and redaction utilities live in `@emdash/shared/logger`.\nWire uses the shared ambient logger to attach call context without passing a\nlogger through every domain function:\n\n```ts\nimport { log, runWithLogger } from '@emdash/shared/logger';\n\nawait runWithLogger(logger.child({ requestId: 'r1' }), async () => {\n  log.info('handling request');\n});\n```\n\nNode entry points can install the `AsyncLocalStorage` store:\n\n```ts\nimport { installAsyncLogContext } from '@emdash/shared/logger/node';\n\ninstallAsyncLogContext();\n```\n\nBrowser and renderer code use a synchronous fallback. It still supports a root\nlogger and scoped synchronous blocks, but it does not preserve context across\n`await`.\n\n## Instrumentation Hooks\n\nUse `WireInstrumentation` (exported from `@emdash/wire/rpc`) for typed events\nthat can be adapted to logs, metrics, or tracing. The seam is threaded through\nthe public options of `serve()`, `connect()`, replicas, and `expose()`:\n\n```ts\nimport type { WireInstrumentation } from '@emdash/wire/rpc';\n\nconst instrumentation: WireInstrumentation = {\n  callEnd: (event) => logger.debug('call finished', event),\n  resyncFailed: (event) => logger.warn('resync failed', event),\n};\n\nserve(transport, controller, { logger, instrumentation });\nconst connection = connect(clientTransport, { instrumentation });\nconst contractClient = client(api, connection);\n```\n\nHooks currently cover:\n\n- procedure start/end, cancellation, and duration.\n- snapshot timing and errors.\n- live topic attach/detach.\n- live model/log resync reasons and resync failures.\n- replica cursor-translation timeouts.\n- mutation dedupe hits.\n- scope cleanup errors.\n- transport connect/disconnect events.\n\nThere are no bundled logger adapters: adapt the typed events to your logging or\nmetrics stack at the application edge.\n\nServing and client options are documented in [API serving](./api/serving.md).\nTransport construction is documented in [API transports](./api/transports.md).\n\n## Scope Logging\n\n`createScope({ logger, label })` attaches a logger to the scope tree. Children\ninherit the logger with a `scope` binding containing their label path:\n\n```ts\nconst runtimeScope = createScope({ label: 'runtime', logger });\nconst sessionScope = runtimeScope.child('session:abc');\n\nsessionScope.log.info('session started');\n```\n\nCleanup errors and failed runs are reported through the scope logger by default.\nUse `describeScope(scope)` for a lightweight label tree when debugging retained\nresources or stuck async work. Scope descriptions include active run labels,\nstart times, and cancellation state. When the scope is created with a custom\n`Clock`, those start times come from that clock, which keeps lifecycle diagnostics\ndeterministic in tests.\n\nScope lifecycle behavior is documented in [runtime lifecycle](./runtime/lifecycle.md)\nand [structured concurrency](./runtime/structured-concurrency.md).\n","packages/wire/docs/runtime/components.md":"# Components\n\n`WireComponent` is the authored abstraction for runtime and service contracts that can be created\nin-process or served from a worker process.\n\nUse a component when a contract has a reusable runtime implementation and the composition root should\nchoose whether it runs in the current process or in a supervised worker. A component is not a\ndependency injection container: it describes one contract implementation, its explicit requirements,\nits config schema, and how to synchronously create a scope-owned instance.\n\n## Define A Component\n\n```ts\nimport { defineContract, procedure, createController } from '@emdash/wire/rpc';\nimport { defineWireComponent, requireContract } from '@emdash/wire/worker';\nimport { z } from 'zod';\n\nexport const clockContract = defineContract({\n  now: procedure({ input: z.void().optional(), output: z.number() }),\n});\n\nexport const counterContract = defineContract({\n  increment: procedure({ input: z.void().optional(), output: z.number() }),\n});\n\nconst component = defineWireComponent({\n  id: 'counter',\n  contract: counterContract,\n  requirements: {\n    clock: requireContract(clockContract),\n  },\n  configSchema: z.object({\n    initialValue: z.number().int().default(0),\n  }),\n  create: ({ scope, dependencies, config, instance }) => {\n    let value = config.initialValue;\n    scope.add(() => {\n      value = 0;\n    });\n\n    return instance({\n      scope,\n      controller: createController(counterContract, {\n        increment: async () => {\n          await dependencies.clock.now(undefined);\n          value += 1;\n          return value;\n        },\n      }),\n    });\n  },\n});\n```\n\n`create` is synchronous. Expected async work belongs in domain procedures, `LiveJobSource`s, machines,\nlive state, or other explicit runtime APIs. Directory creation, installs, probing, and startup\noperations should expose their own lifecycle and errors rather than hiding inside component\nconstruction.\n\n`instance({ scope, controller })` adapts the controller into a typed in-process client using a memory\ntransport. The controller should be unvalidated; the component creation boundary applies validation\naccording to the caller's `validate` policy.\n\n## Explicit Requirements\n\nRequirements are plain object keys. A component can require another Wire contract with\n`requireContract(contract)`. Deployment values such as paths, limits, executable names, and feature\nflags belong in typed component config, not dependency requirements.\n\nThere is no container, provider registry, singleton policy, recursive construction, or automatic\nresolution. Composition roots create or spawn the dependencies they want and pass dependency clients\nexplicitly. Worker hosts may also accept dependency controllers and forward them over dependency\nchannels.\n\n```ts\nconst clock = defineWireComponent({\n  id: 'clock',\n  contract: clockContract,\n  requirements: {},\n  configSchema: z.object({}),\n  create: ({ instance, scope }) =>\n    instance({\n      scope,\n      controller: createController(clockContract, {\n        now: () => Date.now(),\n      }),\n    }),\n});\n```\n\n## In-Process Creation\n\n`component.create({ scope, dependencies, config, validate })` validates config and dependency keys,\ncreates a child scope, wires a memory transport, serves the controller, and returns a typed client.\nDisposing the instance stops the in-memory server and disposes the component scope.\n\nController factories used by components should return unvalidated controllers. The component\ncreation or worker serving boundary applies validation once.\n\n```ts\nimport { createScope } from '@emdash/shared/concurrency';\n\nconst scope = createScope({ label: 'app' });\nconst clockInstance = clock.create({\n  scope,\n  dependencies: {},\n  config: {},\n  validate: 'inputs',\n});\n\nconst counterInstance = component.create({\n  scope,\n  dependencies: {\n    clock: clockInstance.client,\n  },\n  config: {\n    initialValue: 41,\n  },\n  validate: 'inputs',\n});\n\nawait counterInstance.client.increment(undefined); // 42\nawait scope.dispose();\n```\n\nThe caller owns the parent scope. Each component creates a child scope under it. Disposing either the\ninstance or the parent scope releases the component resources.\n\n## Worker Deployment\n\n`WireWorkerHost.create(component, options)` returns a lazy supervised worker. `worker.ready()`\nstarts it and returns the stable typed client. `spawn(component, options)` eagerly starts the\nworker and returns that client.\n\nWorker component IPC uses internal framed logical channels:\n\n- `runtime` for the served component contract.\n- bootstrap messages for config and readiness.\n- one `dep:<name>` channel per contract requirement.\n\nDependency channels are forwarded only from the explicit `dependencies` object supplied by the\ncomposition root. Child workers receive typed clients for those channels; they do not locate or\nstart dependencies themselves.\n\nParent process:\n\n```ts\nimport { createScope } from '@emdash/shared/concurrency';\nimport { createWireWorkerHost } from '@emdash/wire/worker';\nimport { childProcessSpawner } from '@emdash/wire/worker/node';\nimport { clock, component as counterComponent } from './component';\nimport { workerPath } from './worker-manifest';\n\nconst scope = createScope({ label: 'app' });\nconst host = createWireWorkerHost({\n  scope,\n  processSpawner: childProcessSpawner(),\n});\n\nconst clockInstance = clock.create({\n  scope,\n  dependencies: {},\n  config: {},\n});\n\nconst counterWorker = host.create(counterComponent, {\n  executable: workerPath('counter'),\n  dependencies: {\n    clock: clockInstance.client,\n  },\n  config: {\n    initialValue: 0,\n  },\n});\n\nconst counterClient = await counterWorker.ready();\nawait counterClient.increment(undefined);\n```\n\nWorker entry:\n\n```ts\nimport { runWireComponentWorker } from '@emdash/wire/worker';\nimport { component as counterComponent } from './component';\n\nvoid runWireComponentWorker(counterComponent);\n```\n\nThe client returned by `ready()` is stable across process generations. Calls fail while the worker is\nunavailable; the client does not buffer calls during restarts.\n\n## Unrecoverable Failures\n\nComponent code can call `fatal(error)` when process-local state is poisoned and the current instance\ncannot safely serve later requests. Fatal failures retire the component scope exactly once. An\nin-process caller can observe the failure through `onFatal`; `runWireComponentWorker` exits with code\n`1`, allowing the owning `WireWorkerHost` to replace that process generation under its supervision\npolicy.\n\nUse `fatal` only when replacing the instance is the recovery mechanism, such as a native operation\nthat remains pending after its deadline and cannot be cancelled in-process. Expected request errors\nshould still be returned or thrown through the contract. A fatal report does not make an unresponsive\nnative call safe to overlap with another call in the same process; worker deployment recovers by\nterminating the process that owns the native state.\n\n## Testing Components\n\nFor in-process tests, create the component under a test scope and pass fake dependency clients\nexplicitly.\n\n```ts\nimport { createScope } from '@emdash/shared/concurrency';\nimport { createController } from '@emdash/wire/rpc';\nimport { defineWireComponent } from '@emdash/wire/worker';\nimport { z } from 'zod';\n\nconst scope = createScope({ label: 'test' });\nconst fakeClockComponent = defineWireComponent({\n  id: 'fake-clock',\n  contract: clockContract,\n  requirements: {},\n  configSchema: z.object({}),\n  create: ({ instance, scope }) =>\n    instance({\n      scope,\n      controller: createController(clockContract, {\n        now: () => 1,\n      }),\n    }),\n});\nconst fakeClock = fakeClockComponent.create({\n  scope,\n  dependencies: {},\n  config: {},\n});\n\nconst counter = component.create({\n  scope,\n  dependencies: {\n    clock: fakeClock.client,\n  },\n  config: {\n    initialValue: 0,\n  },\n  validate: 'full',\n});\n\nawait expect(counter.client.increment(undefined)).resolves.toBe(1);\nawait scope.dispose();\n```\n\nFor worker tests, use `FakeWorkerProcessSpawner` and run the child side with\n`runWireComponentWorker(component, { port: fakeProcess.childPort })`. This exercises bootstrap,\nlogical channel isolation, dependency forwarding, readiness, and reconnect behavior without starting\na real child process.\n\n## Build Metadata\n\nManual worker manifests in the desktop app and workspace-server are temporary build metadata. They\nmap thin entry files to emitted worker artifacts until a bundler plugin owns worker artifact\ngeneration. They are not part of the component runtime API.\n\n## Guidelines\n\n- Keep component factories synchronous and cheap.\n- Report poisoned process-local state through `fatal`; keep expected operation failures on the\n  contract.\n- Pass dependencies by name from the composition root; never look them up from a global registry.\n- Prefer contract requirements for shared capabilities that may cross process boundaries.\n- Use typed config for deployment-specific values such as paths, limits, executable names, and\n  feature flags.\n- Keep controller validation at the component creation or worker serving boundary, not inside every\n  controller factory.\n- Keep manual worker manifests and entry files thin; they should only select a component and call\n  `runWireComponentWorker(component)`.\n","packages/wire/docs/runtime/lifecycle.md":"# Lifecycle Utilities\n\n`Scope`, `LifecycleRegistry`, and the resource cache primitives are\ndependency-light Shared lifecycle utilities exported from\n`@emdash/shared/concurrency`. Scheduling utilities are exported separately from\n`@emdash/shared/scheduling`. Wire uses these primitives, but they do not define\nWire protocol messages.\n\n- `Scope` owns cleanup and async work for a tree of resources.\n- `LifecycleRegistry` owns explicit keyed start/stop state machines.\n- `Clock` owns sleeps, deadlines, retry backoff, and disposable timers.\n- `ResourceCache` turns keyed demand into retained resources with ref-counted leases.\n- `SharedResource` is the unkeyed form for one lazily created resource.\n- `AsyncCache` caches async values that do not have finalizers.\n- `Mailbox` is a bounded async handoff queue for one logical consumer.\n\nThey do not define wire messages and can run in browser, renderer, main process,\nor tests.\n\n## Scope\n\nUse a scope when a feature creates more than one disposable resource and those\nresources should die together:\n\n```ts\nimport { createScope } from '@emdash/shared/concurrency';\n\nconst scope = createScope({ label: 'conversation:abc', logger });\n\nscope.add(() => detachLiveState());\nscope.add(() => removeWindowListener());\nscope.use({ dispose: () => runtime.dispose() });\nscope.run('refresh', async (signal) => {\n  await clock.sleep(250, { signal });\n  await refreshVisibleState({ signal });\n});\n\nawait scope.dispose();\n```\n\nSemantics:\n\n- `state` is `open`, `closing`, or `closed`.\n- `signal` aborts synchronously when disposal begins.\n- `run(label, fn)` starts a tracked async operation with its own abort signal.\n- Cleanups run in reverse registration order.\n- Child scopes and active runs settle before the parent's own cleanups.\n- Cleanup errors are reported through `onCleanupError` and do not stop later\n  cleanups.\n- `dispose()` is idempotent.\n- `add()` on an already disposed scope runs the cleanup immediately.\n- `Run.exit` never rejects; use `Run.value()` when ordinary promise rejection is\n  desired.\n\nThat last rule makes async attach/dispose races easier to handle:\n\n```ts\nconst scope = createScope();\nawait scope.dispose();\n\nscope.add(() => detachTopic()); // runs immediately\n```\n\n## Scope Logging\n\nEvery scope has `scope.log`. If a logger is supplied at the root, children\ninherit it with a `scope` binding for the accumulated label path:\n\n```ts\nconst runtimeScope = createScope({ label: 'runtime', logger });\nconst sessionScope = runtimeScope.child('session:one');\n\nsessionScope.log.info('session attached');\n```\n\nDefault cleanup error handling logs through the scope logger:\n\n```ts\nconst scope = createScope({ label: 'view', logger });\nscope.add(() => {\n  throw new Error('cleanup failed');\n});\n\nawait scope.dispose(); // logger.warn('scope cleanup failed', ...)\n```\n\nCustom cleanup handlers receive `{ label, labelPath, logger }`:\n\n```ts\nconst scope = createScope({\n  label: 'root',\n  onCleanupError: (error, context) => {\n    context.logger.warn('custom cleanup failure', {\n      labelPath: context.labelPath,\n      error,\n    });\n  },\n});\n```\n\nUse `describeScope(scope)` to inspect the live label tree:\n\n```ts\nconsole.log(describeScope(runtimeScope));\n```\n\nThe description contains labels, label paths, lifecycle state, active runs, and\nchild descriptions. It does not expose cleanup callbacks.\n\nWhen `createScope({ clock })` is used, child scopes inherit the same clock and\nrun diagnostics use deterministic timestamps.\n\n## Scope Runs\n\nUse `run()` when async work must not outlive the scope:\n\n```ts\nconst run = sessionScope.run('attach-transcript', async (signal) => {\n  const detach = await transcript.attach(updateView, { signal });\n  sessionScope.add(detach);\n});\n\nawait run.exit;\n```\n\nDisposing `sessionScope` cancels the run and waits for it before running the\nscope's cleanups. Cancellation is cooperative: pass the provided signal to any\noperation that can wait, sleep, perform I/O, or subscribe.\n\nUse `run()` for background jobs, process transitions, replica attachment,\nrenderer bindings, and async setup. Use `add()` or `use()` for finalizers. Use\n`ResourceCache` when a keyed resource should be shared by leases.\n\nFor the full lifecycle model and invariants, see\n[Structured concurrency](./structured-concurrency.md).\n\n## LifecycleRegistry\n\nUse `LifecycleRegistry` when a feature owns a keyed set of resources that are\nstarted and stopped by explicit commands and callers need observable lifecycle\nstate:\n\n```ts\nimport { createLifecycleRegistry } from '@emdash/shared/concurrency';\nimport { ok } from '@emdash/shared';\n\nconst runtimes = createLifecycleRegistry<\n  { workspaceId: string },\n  Runtime,\n  StartRuntimeError,\n  { reason: string },\n  StopRuntimeError\n>({\n  label: 'workspace-runtimes',\n  scope: appScope,\n  keyOf: (input) => input.workspaceId,\n  start: async (input, scope, signal) => {\n    const runtime = await createRuntime(input.workspaceId, { signal });\n    scope.add(() => runtime.dispose());\n    return ok(runtime);\n  },\n  stop: async (_key, runtime, context, _scope, signal) => {\n    await runtime.stop({ reason: context?.reason, signal });\n    return ok(undefined);\n  },\n  onStateChanged: ({ key, current }) => {\n    logger.info('runtime state changed', { key, state: current.kind });\n  },\n});\n```\n\nSelection guidance:\n\n- Use `Scope` for ownership, cancellation, cleanup ordering, and async work that\n  must not outlive its feature. It is not a keyed start/stop registry.\n- Use `LifecycleRegistry` for local keyed resources with explicit `start()`,\n  `stop()`, `register()`, `state()`, and state-change observation.\n- Use `ResourceCache` when demand is lease-driven and the resource should exist\n  only while consumers hold leases or while an idle TTL retains it.\n- Use `WireWorkerHost.create(component, ...)` when the resource is a Wire worker process with\n  readiness, a stable typed client returned by `ready()`, supervision, restart backoff, and child\n  process generations.\n- Use `LiveJobSource` when callers need a Wire-visible cancellable job with progress,\n  terminal state, retention, and remote client handles.\n\n`LifecycleRegistry` differs from `Scope` by modeling resource state rather than\ngeneral cleanup. It differs from `ResourceCache` by not ref-counting leases or\ncreating resources on demand from `acquire()`. It differs from Wire workers and\n`LiveJobSource` by staying local and protocol-free: it does not supervise processes,\nserve clients, publish progress, or retain terminal job state.\n\n## Mailbox Ownership\n\nUse `Mailbox` when local producers and one consumer need an explicit bounded\nhandoff. Register it with the owning scope, then run the consumer loop under the\nsame scope:\n\n```ts\nimport { createMailbox } from '@emdash/shared/concurrency';\n\nconst mailbox = sessionScope.use(createMailbox<Event>({ capacity: 256 }));\n\nsessionScope.run('drain-events', async () => {\n  for await (const event of mailbox) {\n    await handleEvent(event);\n  }\n});\n```\n\nDisposing `sessionScope` disposes the mailbox and unblocks pending producers or\nconsumers. For state machines, overflow guarantees, and the deferred Broadcast\ncontract, see [Mailbox and Broadcast](./mailbox-and-broadcast.md).\n\n## Child Scopes\n\nUse child scopes to model ownership:\n\n```ts\nconst runtimeScope = createScope({ label: 'runtime' });\nconst sessionScope = runtimeScope.child('session:one');\n\nsessionScope.add(() => stopSession());\nruntimeScope.add(() => stopRuntime());\n\nawait runtimeScope.dispose();\n```\n\nDisposing `runtimeScope` disposes `sessionScope` first, then `stopRuntime()`.\nDisposing `sessionScope` directly deregisters it from the parent so it does not\ndispose twice later.\n\n## ResourceCache\n\n`ResourceCache` is useful when a resource should exist only while somebody is\nusing it: a live topic binding, a renderer view model, a preview server, or a\nprocess-backed session.\n\n```ts\nimport { createResourceCache } from '@emdash/shared/concurrency';\n\nconst sessions = createResourceCache({\n  scope: runtimeScope,\n  label: 'sessions',\n  key: (input: { conversationId: string }) => input.conversationId,\n  idleTtlMs: 30_000,\n  create: async ({ conversationId }, scope) => {\n    const session = await startSession(conversationId);\n    scope.add(() => session.stop());\n    return session;\n  },\n  onError: (error, key) => logger.warn('session creation failed', { key, error }),\n});\n```\n\n`create(key, scope)` is also the disposal hook. Register teardown on the supplied\nscope as soon as resources are acquired. If creation later fails, the scope is\ndisposed and partial resources are cleaned up.\n\nBehavior:\n\n- The first `acquire()` for a key calls `create()`.\n- Concurrent acquires for the same key share the in-flight creation.\n- In-flight creation is tracked as a scope run and is cancelled when the entry is\n  invalidated or the parent scope closes.\n- Each lease increments the refcount.\n- `release()` is idempotent.\n- The last release starts the grace timer.\n- Acquire during the grace timer cancels teardown and reuses the active value.\n- Failed creation is not cached; the next acquire retries.\n- `peek(key)` returns the active value if creation has completed.\n- `dispose()` force-disposes every active or retained entry.\n- Passing `scope` attaches the source and all keyed entries to a parent scope.\n  Disposing that parent scope force-disposes the source and rejects later\n  acquisitions. `label` names the source node in `describeScope()` output.\n- Every input that affects provisioning must be in `K` or fixed in the factory\n  closure. There is no acquire-time creation context.\n\nUsage:\n\n```ts\nconst lease = sessions.acquire({ conversationId: 'abc' });\nconst session = await lease.ready();\n\nawait lease.release();\nawait sessions.dispose();\n```\n\nParent scopes are useful for runtimes with several keyed resources:\n\n```ts\nconst authScope = runtimeScope.child('auth');\nconst logins = createResourceCache({\n  scope: authScope,\n  label: 'login-source',\n  key: (key: { providerId: string; methodId: string; generation: string }) =>\n    `${key.providerId}:${key.methodId}:${key.generation}`,\n  create: async (key, scope) => {\n    const login = await startLoginPty(key.providerId, key.methodId);\n    scope.add(() => login.dispose());\n    return login;\n  },\n});\n\nawait authScope.dispose(); // also disposes every active login entry\n```\n\n## Grace Windows\n\nUse `idleTtlMs` for resources whose demand can flap briefly. For example, a window\nreload may detach and reattach live model bindings within a few hundred\nmilliseconds. A short grace window avoids tearing down the underlying resource\nonly to recreate it immediately.\n\n```ts\nconst bindings = createResourceCache({\n  key: (key: { taskId: string }) => key.taskId,\n  idleTtlMs: 5_000,\n  create: async (key, scope) => {\n    const binding = client.task.transcript(key, (state) => render(state));\n    scope.add(() => binding.dispose());\n    await binding.ready;\n    return binding;\n  },\n});\n```\n\nUse `ResourceCache`, `SharedResource`, or `AsyncCache` for new shared lifetime\nwork. Avoid adding compatibility lifecycle adapters around new code.\n\nSee [../../examples/scope/client.ts](../../examples/scope/client.ts) and\n[../../examples/resource-cache/client.ts](../../examples/resource-cache/client.ts).\n","packages/wire/docs/runtime/mailbox-and-broadcast.md":"# Mailbox and Broadcast\n\n`Mailbox<T>` is a Shared bounded asynchronous handoff primitive exported from\n`@emdash/shared/concurrency`. Wire uses it when one logical consumer needs to\ndrain values from one or more producers without hand-writing arrays, waiter\nlists, terminal flags, and overflow policy.\n\n```ts\nimport { createMailbox, createScope } from '@emdash/shared/concurrency';\n\nconst scope = createScope({ label: 'download' });\nconst chunks = scope.use(createMailbox<Uint8Array>({ capacity: 64 }));\n\nconst consumer = scope.run('drain', async () => {\n  for await (const chunk of chunks) {\n    await writeChunk(chunk);\n  }\n});\n\nawait chunks.offer(new Uint8Array([1, 2, 3]));\nchunks.close();\nawait consumer.value();\n```\n\n## Mailbox\n\nA mailbox has many producers and one logical consumer. Values accepted into the\nmailbox are delivered in FIFO order. The consumer can use `take()` for one value\nat a time or `for await` for a draining loop.\n\n```ts\nconst mailbox = createMailbox<Job>({\n  capacity: 128,\n  overflow: 'suspend',\n});\n\nawait mailbox.offer(job, { signal });\nconst next = await mailbox.take({ signal });\n```\n\n### State Machine\n\n```mermaid\nstateDiagram-v2\n  [*] --> Open\n  Open --> Open: offer / take\n  Open --> Closing: close()\n  Closing --> Closed: buffer drained\n  Open --> Failed: fail(error)\n  Failed --> Failed: drain accepted values\n  Failed --> [*]: next take throws error\n  Closed --> [*]\n```\n\n`close()` is graceful: accepted values drain before consumers see the end of the\nstream. `fail(error)` is also ordered: accepted values drain first, then the next\ntake or iterator step throws the failure. `dispose()` is immediate: it clears\nbuffered values and unblocks pending producers and consumers.\n\n### Guarantees\n\n- Accepted values are delivered in FIFO order.\n- A value is delivered to at most one consumer.\n- Only one pending `take()` or active async iterator is allowed at a time.\n- Capacity is explicit and always finite.\n- Overflow is explicit: `suspend`, `reject`, `drop-oldest`, or `drop-newest`.\n- `tryOffer()` never waits.\n- `offer()` waits only when overflow is `suspend`.\n- `offer()` and `take()` can observe an `AbortSignal`; aborting one operation does\n  not close the mailbox.\n- Terminal operations are idempotent.\n- `scope.use(mailbox)` ties disposal to the owning scope, but consumer loops should\n  still run under `scope.run()` when they must not outlive the feature.\n\n### Overflow Policies\n\nUse `suspend` when every accepted value matters and producers can wait. Use\n`reject` when overflow is a protocol or invariant violation. Use `drop-oldest` for\nbest-effort queues where freshness matters more than history. Use `drop-newest`\nwhen existing queued work should be preserved and incoming work can be ignored.\n\nBlob channels use `reject` internally because dropping bytes would corrupt the\nstream. The reconnecting transport uses a shared bounded buffer with\n`drop-oldest`, preserving the existing policy that old non-blob frames are less\nvaluable than recent ones while disconnected.\n\n### Non-Goals\n\nMailbox is not:\n\n- a durable queue;\n- a multi-consumer work queue;\n- a replay log;\n- an acknowledgement or retry protocol;\n- a mutex;\n- a domain error channel.\n\nExpected domain failures should still be represented as `Result<T, E>`. Mailbox\nfailure means the local handoff itself was terminated by lifecycle, protocol, or\ntransport failure.\n\n## Broadcast Contract\n\n`Broadcast<T>` is the reserved name for a future local asynchronous fan-out\nprimitive. It should not be called `Topic<T>` because Wire already uses “topic” for\nremote live subscription addresses.\n\nThe intended shape is one mailbox per subscriber:\n\n```mermaid\nflowchart LR\n  publisher[Publisher] --> broadcast[Broadcast]\n  broadcast --> subA[\"Subscriber A Mailbox\"]\n  broadcast --> subB[\"Subscriber B Mailbox\"]\n  broadcast --> subC[\"Subscriber C Mailbox\"]\n```\n\nThe intended guarantees are:\n\n- `publish(value)` never runs subscriber application code directly.\n- Each subscriber receives values through its own mailbox.\n- Slow subscribers apply only their own overflow policy.\n- Per-subscriber delivery is FIFO for accepted values.\n- New subscribers do not receive replay.\n- Closing or failing the broadcast propagates to every subscription.\n- Publish results report subscriber count, delivered count, and dropped count.\n\nDo not implement or migrate to `Broadcast<T>` until a source has at least two\ngenuinely asynchronous consumers that need independent buffering or overflow\npolicies. Current Wire sources do not meet that bar: process stdio has one\nproduction subscriber, and observability hooks already isolate synchronous\nfailures.\n\n## Where Not To Use These\n\nDo not replace `LiveStateSource`, `LiveLogSource`, `EventStreamSource`, replicas, live\nfollowers, or reconnect attachment logic with Mailbox or Broadcast. Those\nprimitives carry generation, sequence, snapshot, resync, reconnect, retention, or\ngap semantics that Mailbox deliberately does not provide.\n\nDo not replace condition waiters such as live cursor waiters or renderer version\nwaiters. They wait for a predicate over monotonic state; they do not queue values.\n\nSee also [Structured concurrency](./structured-concurrency.md) and\n[Lifecycle utilities](./lifecycle.md).\n","packages/wire/docs/runtime/resource-cache.md":"# Resource Caches\n\n`ResourceCache`, `SharedResource`, and `AsyncCache` are Shared primitives from\n`@emdash/shared/concurrency`. Wire uses them for shared lifetimes and cached\nasync values, but they do not define Wire protocol messages.\n\n## Choose A Primitive\n\n- Use `ResourceCache<K, T>` for keyed resources that need leases and teardown:\n  process sessions, replicas, file watchers, tree resources, and connection pools.\n- Use `SharedResource<T>` for one lazily created resource with the same lease\n  semantics: a lazy worker, singleton client, or shared runtime helper.\n- Use `AsyncCache<K, T>` for data without finalizers: auth status probes,\n  metadata lookups, or other retryable reads.\n- Use `deduplicate()` middleware only when you want in-flight sharing without\n  retaining successful values.\n\n## Resource Identity\n\nEvery input that can change provisioning must be part of `K` or captured by the\nfactory closure. The cache only receives `acquire(key)`, so there is no separate\ncreation context:\n\n```ts\nconst logins = createResourceCache({\n  key: (key: { providerId: string; methodId: string; generation: string }) =>\n    `${key.providerId}:${key.methodId}:${key.generation}`,\n  create: async (key, scope) => {\n    const pty = await startLoginPty(key.providerId, key.methodId);\n    scope.add(() => pty.dispose());\n    return pty;\n  },\n});\n```\n\n## Scope Ownership\n\nEach cache owns an internal scope. Each entry gets a child scope, and creation runs\nunder `scope.run('create', ...)`. Register teardown on the entry scope as soon as\nresources are acquired. If creation fails, the entry scope is disposed and the\nnext acquire retries.\n\n`invalidate(key)` and `dispose()` force-close entries even if leases are still\nheld. Releasing a stale lease later is a no-op. This keeps owner shutdown from\nwaiting forever on leaked consumers.\n\n## Lease Behavior\n\n- Concurrent `acquire(key)` calls share one in-flight creation.\n- Every acquire receives its own idempotent lease.\n- `peek(key)` returns only completed active values.\n- Failed creation is not cached.\n- By default, releasing the final lease during creation retains the in-flight entry so a later\n  acquire can share it and the factory's terminal result remains authoritative.\n- Set `cancelPendingOnRelease: true` for resources such as watchers that have no useful lifetime\n  without a lease. Pending work must then observe the entry scope's signal and clean up late results.\n- `idleTtlMs` retains a zero-ref entry briefly so flapping demand can reuse it.\n- Acquiring during teardown waits for teardown to finish, then provisions fresh.\n\n## AsyncCache\n\n`AsyncCache` shares concurrent loads and caches only successes:\n\n```ts\nconst statuses = createAsyncCache({\n  key: (providerId: string) => providerId,\n  ttlMs: 15 * 60 * 1000,\n  load: (providerId, signal) => checkAuthStatus(providerId, { signal }),\n});\n```\n\n`get()` uses a cached value when fresh, `refresh()` cancels the current load and\nstarts a new generation, `set()` publishes an explicit value, and failures are\nnever retained.\n","packages/wire/docs/runtime/scheduling.md":"# Scheduling\n\n`@emdash/shared/scheduling` centralizes time, retry, and\ncancellation-sensitive waits. Wire uses these Shared primitives, and runtime\ncode should depend on `Clock` instead of calling platform timer APIs directly.\n\n```ts\nimport { retry, retrySchedules, systemClock } from '@emdash/shared/scheduling';\n\nawait retry(\n  () => transport.connect(),\n  {\n    clock: systemClock,\n    schedule: retrySchedules.exponential({ initialMs: 100, maxMs: 2_000 }),\n    shouldRetry: (error) => isTransient(error),\n  }\n);\n```\n\n## Clock\n\n`Clock` has three operations:\n\n- `now()` returns wall-clock milliseconds for diagnostics and deadlines.\n- `schedule(delayMs, callback)` returns a disposable `TimerHandle`.\n- `sleep(delayMs, { signal })` returns an abortable promise.\n\nUse `Clock` for sleeps, backoff, debounce, retention, idle TTLs, readiness\ndeadlines, and test polling. Keep direct `Date.now()` or `performance.now()` for\nprotocol timestamps, generated IDs, and duration measurement where the wall clock\nor monotonic timer is the data being recorded.\n\n## TimerHandle Ownership\n\nEvery scheduled callback must have one owner. Register the handle with a `Scope`,\nstore it on the owning entry, or dispose it in all terminal paths:\n\n```ts\nconst handle = clock.schedule(5_000, () => expire(id), { unref: true });\nscope.add(() => handle.dispose());\n```\n\n`TimerHandle.dispose()` is idempotent. The `active` flag becomes false before the\ncallback runs, so callback and disposal races remain at-most-once.\n\n## Timeouts\n\n`runWithTimeout(work, { timeoutMs, signal, clock })` derives a child\n`AbortSignal`, schedules a deadline on the provided `Clock`, and disposes its\ntimer in every terminal path. The returned promise rejects with `TimeoutError`\nwhen the deadline expires, but parent cancellation still rejects with the\nparent's abort reason.\n\n```ts\nconst result = await runWithTimeout(\n  (signal) => loadWorkspace({ signal }),\n  { timeoutMs: 5_000, signal: parentSignal, clock }\n);\n```\n\nThe timeout rejects promptly even when work ignores the derived signal, but the\nunderlying JavaScript work can only stop cooperatively. Pass the derived signal\ninto sleeps, process waits, transports, and any helper that can observe\ncancellation.\n\n## ManualClock\n\nTests should import `ManualClock` from `@emdash/shared/testing`. It preserves FIFO\nordering for timers with the same deadline and flushes resumed promise\nmicrotasks while advancing:\n\n```ts\nconst clock = new ManualClock(1_000);\nconst done = clock.sleep(250);\n\nawait clock.advanceBy(250);\nawait done;\n```\n\nUse `runAll({ maxTimers })` only when the test expects the scheduled work to\ndrain. The guard prevents accidental infinite timer loops.\n\n## Retry Semantics\n\n`RetrySchedule.delayFor(0)` is the delay before the first retry. Attempt `0` is\nthe initial operation call, attempt `1` is the first retry, and so on.\n`undefined` means retries are exhausted.\n\nSchedules are stateless and composable: fixed, finite sequence, repeat-last\nsequence, exponential, limited, and deterministic jitter. Inject a deterministic\nrandom function into jittered schedules in tests.\n\n`retry()` is exception-based. Domain errors that are expected API results should\nremain `Result<T, E>` values; use `shouldRetry` to classify thrown transport or\nruntime failures.\n\n## Wire Retry Vocabulary\n\nWire expresses all retry timing through one schedule vocabulary:\n`RetrySchedule` and the `retrySchedule()` constructor from\n`@emdash/shared/scheduling`. The reconnecting transport's reconnect delays,\nworker-slot supervision, the follower's `resyncRetry` policy, and per-call\nopt-in retries all consume it. Shared vocabulary, not shared state — each site\nkeeps its own independently tunable schedule:\n\n```ts\nimport { retrySchedule } from '@emdash/shared/scheduling';\n\nconst supervision = {\n  restart: 'on-failure',\n  schedule: retrySchedule({ delaysMs: [250, 1_000, 2_500], repeatLast: true, maxRetries: 5 }),\n};\n```\n\n## Periodic Work\n\nPrefer abortable loops or chained one-shot timers over overlapping intervals.\nThat keeps disposal ordered and avoids installing work after an owner has begun\nclosing:\n\n```ts\nscope.run('poll', async (signal) => {\n  while (!signal.aborted) {\n    await refresh({ signal });\n    await clock.sleep(1_000, { signal });\n  }\n});\n```\n","packages/wire/docs/runtime/structured-concurrency.md":"# Structured Concurrency\n\nShared `Scope.run()` from `@emdash/shared/concurrency` tracks asynchronous work\nunder the same owner as subscriptions, processes, timers, and other resources.\nWire uses it when work must not outlive the feature that started it.\n\n```ts\nconst scope = createScope({ label: 'session:abc', logger, clock });\n\nconst run = scope.run('load-history', async (signal) => {\n  const history = await loadHistory({ signal });\n  return history;\n});\n\nconst history = await run.value();\nawait scope.dispose();\n```\n\n## Concepts\n\n- `Scope` owns a lifecycle tree. Parent disposal closes children.\n- `Run<A>` is one async operation owned by a scope.\n- `Run.exit` never rejects. It resolves to `success`, `failure`, or `cancelled`.\n- `Run.value()` converts the exit back to normal promise semantics.\n- `Lease<A>` is temporary use of a shared resource, usually from `ResourceCache`\n  or `SharedResource`.\n\n```mermaid\nflowchart TD\n  featureScope[\"Feature Scope\"] --> childScope[\"Child Scope\"]\n  featureScope --> runA[\"Run: load-history\"]\n  childScope --> runB[\"Run: attach-topic\"]\n  childScope --> cleanup[\"Finalizers\"]\n```\n\n## Lifecycle\n\nDisposal is idempotent and ordered:\n\n1. The scope moves from `open` to `closing`.\n2. `scope.signal` aborts synchronously.\n3. Existing child scopes begin closing.\n4. Active runs are cancelled.\n5. The scope waits for child scopes and runs.\n6. Cleanups run in reverse registration order.\n7. Cleanup errors are logged and later cleanups still run.\n8. The scope moves to `closed`.\n\nStarting a run after closing has begun does not invoke the operation. The returned\nrun is already cancelled. Adding a cleanup after a scope is closed runs it\nimmediately as best-effort cleanup.\n\n## Invariants\n\n- Every child scope has one parent.\n- Every run belongs to one scope.\n- `dispose()` returns the same promise for every caller.\n- `scope.signal` aborts before the first `await` in disposal.\n- Parent cancellation propagates to children.\n- Sibling runs do not cancel each other.\n- `Run.exit` never rejects.\n- A cancelled run is reported as `cancelled` even if the operation later returns.\n- A failed run is logged through the scope logger.\n- A scope is not closed until owned runs and children have settled.\n\nJavaScript cancellation is cooperative. A run that ignores its signal can keep\n`dispose()` pending. Pass the run signal into network calls, sleeps, process\nwaits, and any helper that can observe cancellation.\n\n`Scope` accepts an optional `Clock`. Child scopes inherit it, and active-run\ndiagnostics use `clock.now()` for deterministic timestamps in tests. Prefer\n`clock.sleep(ms, { signal })` for run-owned delays so disposal can interrupt the\nwait immediately.\n\nA run must not await disposal of its own owning scope. That creates a cycle:\n`dispose()` waits for the run, while the run waits for `dispose()`.\n\n## When To Use It\n\nUse `run()` for:\n\n- background jobs and progress sources;\n- pending replica attachment or snapshot work;\n- process spawn, restart backoff, and exit handling;\n- renderer bindings with fire-and-forget RPCs;\n- async setup that must be cleaned up if later setup fails.\n\nUse `add()` or `use()` for:\n\n- subscriptions;\n- timers and observers;\n- resource finalizers;\n- objects with `dispose()`.\n\nUse child scopes for:\n\n- per-session resources under a runtime;\n- per-job resources under a job source;\n- per-consumer resources under a shared cache;\n- modal or view-local lifetimes.\n\nUse `ResourceCache` for keyed, ref-counted resources. Its creation runs under\nthe entry scope, so invalidating an entry or disposing the parent scope cancels\nand joins in-flight creation before finalizers run.\n\nUse `Mailbox` for bounded asynchronous handoff between producers and one logical\nconsumer. The mailbox owns buffering and terminal-state wakeups; run the consumer\nloop under `scope.run()` when it must not outlive the feature:\n\n```ts\nconst mailbox = scope.use(createMailbox<Message>({ capacity: 128 }));\n\nscope.run('drain messages', async () => {\n  for await (const message of mailbox) {\n    await handleMessage(message);\n  }\n});\n```\n\n`Broadcast<T>` is documented as a future local fan-out primitive for multiple\nasynchronous consumers with independent backpressure. Do not use Mailbox or\nBroadcast to replace live-state snapshots, retained logs, event streams, replica\ncursors, or reconnect attachment logic; those protocols carry generation,\nsequence, retention, or resync semantics.\n\nDo not use `run()` as:\n\n- a mutex or serialization primitive;\n- a retry policy;\n- a cache;\n- a domain error channel.\n\nExpected domain failures should still be returned as `Result<T, E>`. A run\nfailure means the operation itself threw unexpectedly.\n\n## Diagnostics\n\n`describeScope(scope)` includes the lifecycle state, active child scopes, and\nactive runs:\n\n```ts\nconsole.log(describeScope(scope));\n```\n\nEach run description includes its label, start time, and whether cancellation has\nstarted. This is useful for finding retained sessions, stuck process transitions,\nor long-running renderer bindings during tests and debugging.\n","packages/wire/docs/runtime/workers.md":"# Workers\n\nA worker is a named, process-hosted `WireComponent` owned by a `WireWorkerHost`.\nThe host creates logical workers and returns a stable typed client from readiness. Internally, each worker has one\nslot that owns readiness, supervision, reconnects, graceful shutdown, and process generations.\nLow-level spawners create exactly one process generation.\n\nSee [Components](./components.md) for the authored component shape, local creation examples,\ndependency requirements, and testing guidance. This page focuses on process supervision.\n\n## Layers\n\n```mermaid\nflowchart LR\n  App[\"Application composition\"] --> Host[\"WireWorkerHost\"]\n  Host --> Slot[\"Internal worker slot\"]\n  Slot --> Link[\"Internal worker link\"]\n  Slot --> Spawner[\"WorkerProcessSpawner\"]\n  Spawner --> Process[\"WorkerProcess\"]\n  Process --> Runner[\"runWireComponentWorker\"]\n  Runner --> Controller[\"Component Controller\"]\n  Host --> Deps[\"Explicit dependency channels\"]\n```\n\n- `WireWorkerHost` is a process ownership boundary. Composition roots inject a\n  `WorkerProcessSpawner`, logger, clock, and defaults.\n- The internal worker slot is the only multi-generation state machine. It keeps one stable\n  connection/client while child processes restart underneath it; it is not an author-facing API.\n- `WorkerProcessSpawner` is platform-specific and creates one generation. Use\n  `childProcessSpawner()` from `@emdash/wire/worker/node` for Node child\n  processes.\n- `runWireComponentWorker(component)` is the child-side IPC bridge. It bootstraps typed config,\n  connects explicit dependency clients, creates the component, serves its controller, and signals\n  readiness.\n\n## Parent Side\n\n```ts\nimport { createWireWorkerHost } from '@emdash/wire/worker';\nimport { childProcessSpawner } from '@emdash/wire/worker/node';\nimport { createScope } from '@emdash/shared/concurrency';\nimport { counterComponent } from './component';\nimport { workerPath } from './worker-manifest';\n\nconst scope = createScope({ label: 'main' });\nconst host = createWireWorkerHost({\n  scope,\n  processSpawner: childProcessSpawner(),\n});\n\nconst worker = host.create(counterComponent, {\n  name: 'counter',\n  executable: workerPath('counter'),\n  dependencies: {},\n  config: {},\n});\n\nconst client = await worker.ready();\nawait client.increment(undefined);\nawait host.dispose();\n```\n\n`worker.ready()` starts the process, waits for the child ready signal, and returns the stable\ntyped client. The pending worker intentionally exposes no public client. Calls through a previously\nreturned client fail fast while the worker is unavailable; Wire does not buffer requests during\ndowntime.\n\nStartup and exposure are explicit composition-site decisions. For eager startup, await\n`worker.ready()` under an owning scope or call `host.spawn(component, options)` to get the client. For Electron\nwindows, compose a forwarding controller and use `exposeWireToWindows()` with a `beforeOpen` hook\nthat awaits `worker.ready()`.\n\n## Child Side\n\n```ts\nimport { runWireComponentWorker } from '@emdash/wire/worker';\nimport { counterComponent } from './component';\n\nvoid runWireComponentWorker(counterComponent);\n```\n\nThe child helper resolves the parent IPC channel, requests bootstrap config and dependency channels,\nserves framed runtime wire messages, sends the ready signal after the component controller is\ninstalled, disposes the child scope on shutdown/disconnect, and exits with code `1` if creation\nfails. It also exits with code `1` when the component reports an unrecoverable failure through\n`fatal(error)`. The worker slot treats that exit like any other failed generation: it disconnects the\nold transport and applies the configured supervision schedule before starting a fresh process.\n","packages/wire/examples/README.md":"# @emdash/wire Examples\n\nThese examples show the public surface of `@emdash/wire`. Every example imports\nthe package entrypoints only (`/rpc`, `/live`, `/state`, via their in-repo\nsource paths) and splits the authoritative server side from the client-side\nbinding so the boundary looks like a real transport without adding an adapter.\n\n## Documentation\n\nFor conceptual docs that explain how the examples fit together, see\n[../docs/README.md](../docs/README.md).\n\nRun them from the repository root:\n\n```bash\npnpm --filter @emdash/wire run example:state-kernel\npnpm --filter @emdash/wire run example:live-log\npnpm --filter @emdash/wire run example:event-stream\npnpm --filter @emdash/wire run example:mailbox\npnpm --filter @emdash/wire run example:cancellation\npnpm --filter @emdash/wire run example:api-definition\npnpm --filter @emdash/wire run example:dedupe\npnpm --filter @emdash/wire run example:job-contract\n```\n\nExamples:\n\n- `state-kernel/` demonstrates the state kernel end to end: `cell` + `family`\n  + `expose` on the server, `remote()` + `observe` + `optimistic` on the\n  client, including an optimistic mutation settling against the authoritative\n  cursor.\n- `live-log/` demonstrates a contract-level `liveLog()` endpoint served by a\n  `LiveLogSource`, consumed through `createLiveLogReplicaCache` and\n  `ReplicaLog`.\n- `event-stream/` demonstrates a contract-level `eventStream()` endpoint served\n  by `createEventStreamHost` with client-side subscribe and gap handling.\n- `mailbox/` demonstrates bounded local producer/consumer handoff, graceful close,\n  and explicit overflow.\n- `cancellation/` demonstrates procedure cancellation with `AbortSignal` and\n  server-side abort on disconnect.\n- `api-definition/` isolates contract definition with `defineContract`,\n  `procedure`, `liveModel`, `liveLog`, and live model contract\n  mutations.\n- `dedupe/` demonstrates server-side `deduplicate()` middleware from\n  `@emdash/shared/requests` for in-flight procedure calls.\n- `job-contract/` demonstrates the contract-level `liveJob()` endpoint with start,\n  progress, cancellation, terminal result, and reattach.\n- `scope/` and `resource-cache/` demonstrate the `@emdash/shared/concurrency`\n  primitives the live layer builds on.\n","packages/wire/src/state/README.md":"# `@emdash/wire` reactive state (`wire/src/state/`)\n\n> **Status: implemented in parallel to `wire/src/live/`.** The primitives in\n> this folder are available as `@emdash/wire/state` and are in production use:\n> desktop feature slices (tasks, workspaces, source-control, editor, projects)\n> author providers with `expose` and consume them with `remote`. See\n> [05-migration.md](./05-migration.md) for the remaining migration plan.\n\nThis folder holds the reactive state kernel for wire: a small set of\nsignals-style primitives that unify how authoritative state is produced,\nderived, and published over the wire — and how replicas of it are consumed on\nthe other side.\n\nIt replaces the *value-reactivity* portion of `wire/src/live/` (the four ad-hoc\n\"a value that changes\" flavors: `LiveStateSource` used directly by features,\n`ComputedLiveState`, deleted `BatchedLiveState`, `bindMachineToLiveState`) and the\nhand-written provider/lease boilerplate around them. It does **not** replace\nthe wire protocol (`live/protocol/`), the replica transport, live logs, event\nstreams, or live jobs — those stay. See\n[04-wire-integration.md](./04-wire-integration.md) for the exact boundary.\n\n## Why\n\nToday every stateful runtime hand-rolls the same machinery: dirty flags,\ndebounce timers, one-in-flight-plus-one-queued refresh collapse, lease\nlifecycles, imperative invalidation fan-out, and per-provider\n`acquireState`/`ready`/`release` plumbing. The former file `TreeResource` spent\n~200 lines on scheduling; `RepositoryResource` manually fans invalidations out to every\ncheckout; every `LeasedLiveModelProvider` implementation repeats the same ~40\nlines. The kernel moves all of that into one tested core and leaves feature\ncode with declarations:\n\n```ts\n// A keyed, DB-backed live query — the whole provider.\nconst taskList = family((key: { projectId: string }, scope) =>\n  query({\n    fetch: () => selectTaskListView(db, key),\n    pokes: [tasksChannel.match(key), conversationsChannel.match(key)],\n    scope,\n  }));\n\nexpose(tasksContract.taskList, taskList);\n```\n\n## The primitives at a glance\n\n| Primitive | One-liner | Doc |\n|---|---|---|\n| `cell` | Writable value; the leaf for in-memory sources | [02](./02-primitives.md#cell) |\n| `derived` | Pure, auto-tracked computation over other reactives | [02](./02-primitives.md#derived) |\n| `query` | Async computed from an *external* source (DB, git, fs); declared invalidation via pokes; write-through via `settle` | [02](./02-primitives.md#query) |\n| `family` | Keyed instantiation of any reactive, refcounted with linger GC | [02](./02-primitives.md#family) |\n| `observe` | The only side-effect edge: subscribe a callback, owned by a `Scope` | [02](./02-primitives.md#observe) |\n| `peek` | Untracked read | [02](./02-primitives.md#peek-and-untracked-reads) |\n| `batch` | Group synchronous writes into one turn with shared metadata | [02](./02-primitives.md#batch) |\n| `pokeChannel` | Named invalidation channel connecting external change signals to queries | [02](./02-primitives.md#pokechannel) |\n| `optimistic` | Pending-edit overlay over an authoritative base: apply-now, ack-prune, rollback-for-free | [02](./02-primitives.md#optimistic) |\n| `pin` / `prefetch` | Explicit demand at lifecycle boundaries: fetch at mount/intent, not at render | [02](./02-primitives.md#pin-and-prefetch) |\n| `expose` | Publish a reactive/family as a wire live model (generates the provider) | [04](./04-wire-integration.md#expose) |\n| `remote` | Consume a wire live model as a reactive family (wraps the replica) | [04](./04-wire-integration.md#remote) |\n\nOwnership and disposal always flow through `Scope` from\n`@emdash/shared/concurrency` — there is no manual `release()` in the common\npath. On the client side, demand is driven by `pin`s owned by lifecycle\nboundaries (view scopes), never by render-time observability — see\n[01-concepts.md §6](./01-concepts.md#demand-is-a-policy-not-a-rendering-accident).\n\n## Testing\n\nThe state module is tested in four deterministic tiers:\n\n- Kernel tests in `core/kernel.test.ts` and `core/family.test.ts` cover turns,\n  dynamic dependencies, error recovery, observer isolation, mutation-id\n  folding, and linger GC.\n- Primitive tests in `query.test.ts`, `optimistic.test.ts`, and `pin.test.ts`\n  cover async races, write-through, TTL rollback, generation pruning, and\n  demand aggregation.\n- Bridge tests in `bridge/expose.test.ts` cover readiness, leases, and\n  derived-hop mutation observation.\n- `bridge/roundtrip.test.ts` runs `expose -> wire transport -> remote ->\n  optimistic` over the real test wire harness to protect read-your-writes and\n  no-flicker behavior end to end.\n\n## Reading order\n\n1. [01-concepts.md](./01-concepts.md) — the mental model: nodes, turns,\n   observation, statuses, the tracked/declared boundary, publish metadata.\n2. [02-primitives.md](./02-primitives.md) — reference for each primitive:\n   signatures, semantics, options, edge cases.\n3. [03-composition.md](./03-composition.md) — how primitives compose; worked\n   examples (git runtime, file tree, DB-backed lists, optimistic overlays);\n   the granularity rule that keeps performance flat.\n4. [04-wire-integration.md](./04-wire-integration.md) — `expose`/`remote`,\n   mutations, read-your-writes cursors, and the exact mapping onto the\n   existing `live/protocol/` machinery.\n5. [05-migration.md](./05-migration.md) — legacy class → primitive mapping,\n   what gets deleted, and the incremental migration order.\n\n## Design lineage\n\nThe API descends from SolidJS (ownership tree, `createResource`'s async\nstates, `untrack`, lazy equality-gated memos, `from()` interop), Jotai/Recoil\n(`atomFamily` → `family`), and TanStack Query (`setQueryData` vs\n`invalidateQueries` → `settle` vs pokes). The one deliberate departure from\nSolid: dependency tracking is automatic only where reads are interceptable\n(`derived` over in-process reactives); at the external-source boundary\n(`query` over DB/fs/git), dependencies are *declared* as pokes so that \"poke\ncoverage = query inputs\" stays a greppable, auditable property.\n"},"files":{"AGENTS.md":"# Project Overview\n\nEmdash is a local-first, cross-platform Electron app for running multiple AI coding\nagents in parallel. Each task is isolated in its own Git worktree and can run locally\nor over SSH, while the desktop app coordinates provider CLIs, ACP chat sessions,\nterminal sessions, issue and PR integrations, diff review, and release packaging.\n\n## Repository Structure\n\nThis is a pnpm workspace monorepo. The Electron app lives in\n`apps/emdash-desktop/` as `@emdash/emdash-desktop`; shared packages live under\n`packages/`. Unless a path is prefixed with `packages/` or another app, app paths\nsuch as `src/...`, `drizzle/`, `scripts/`, and `build/` are relative to\n`apps/emdash-desktop/`.\n\nRepo root:\n\n- `.claude/` - Local Claude agent settings for this checkout.\n- `.github/` - GitHub issue templates, reusable actions, CI, and release workflows.\n- `agents/` - Agent-facing architecture, workflow, convention, integration, and risk docs.\n- `apps/emdash-desktop/` - The Electron desktop app.\n- `apps/workspace-server/` - Remote workspace server and its Docker-based dev stack.\n- `packages/chat-ui/` - Shared transcript and ACP chat renderer with Storybook coverage.\n- `packages/core/` - Transport-agnostic runtime primitives, including ACP session logic.\n- `packages/plugins/` - Agent provider plugin definitions, hooks, and ACP adapters.\n- `packages/shared/` - Shared primitives such as result types, logging, and markdown helpers.\n- `packages/theme/` - Theme token codegen that emits the generated theme CSS.\n- `packages/ui/` - Shared React UI components, theme tokens, recipes, and primitives.\n- `packages/wire/` - Typed wire protocol contracts shared by app and workspace server.\n- `pnpm-workspace.yaml` - Workspace package globs for `apps/*` and `packages/**`.\n- Root config files - `package.json`, `nx.json`, `.nvmrc`, `.oxfmtrc.json`,\n  `.oxlintrc.json`, and lockfile/configuration owned at the workspace root.\n\nInside `apps/emdash-desktop/`:\n\n- `build/` - Electron packaging assets; avoid edits unless working on packaging/signing.\n- `drizzle/` - Generated Drizzle SQL migrations and metadata.\n- `scripts/` - Release, verification, and build support scripts.\n- `src/main/` - Electron main process: bootstrap, Wire gateway, DB, and host services.\n- `src/entry/` - Electron entry points: `main.ts` (main process) and `preload.ts`, the typed\n  preload bridge exposed to the renderer.\n- `src/renderer/` - React composition shell, shared browser infrastructure, and tests.\n- `src/core/` - Vertical slices with portable APIs, Node implementations, browser UI, and manifests.\n- `src/types/` - Ambient and cross-cutting TypeScript declarations.\n- `tooling/` - App-level development and test infrastructure not bundled into production.\n\n## Build & Development Commands\n\nThe only prerequisite is any `pnpm` on PATH: the root `package.json` pins\n`packageManager: pnpm@10.28.2` and `devEngines.runtime` node `24.14.0` with\n`onFail: \"download\"`, so pnpm provisions the pinned toolchain itself (no nvm or\ncorepack). The committed `mise.toml` is an optional convenience for mise users.\nRoot scripts are powered by Nx and run package targets in dependency order with\nlocal caching where configured.\n\nGolden-path commands (see [Developer flows](agents/workflows/flows.md) for the\nfull index — one blessed command per flow, database flows, Storybook,\npackaging, remote development, and escape hatches):\n\n```bash\npnpm install        # complete setup from the repo root — nothing else needed\npnpm run doctor     # report-only environment health check\npnpm run dev        # full workspace dev (root); app-only from apps/emdash-desktop/\npnpm run build      # build all workspace projects\npnpm run check      # full merge gate: format, lint, typecheck, test\n```\n\nThe four gate commands also run individually from the repo root:\n\n```bash\npnpm run format\npnpm run lint\npnpm run typecheck\npnpm run test\n```\n\nUse an isolated development database for schema or migration work:\n\n```bash\nEMDASH_DB_FILE=/tmp/emdash-scratch.db pnpm run dev\n```\n\nRun focused database validation from `apps/emdash-desktop/`:\n\n```bash\npnpm run db:fixtures\npnpm run test:migrations\n```\n\nDeploy releases only when explicitly asked to do release work:\n\n```bash\ngh workflow run release-prod.yml --ref main -f arch=both\ngh workflow run release-canary.yml --ref main -f arch=both\ngh workflow run release-workspace-server.yml --ref main -f channel=stable\n```\n\nProduction releases publish artifacts to GitHub Releases and Cloudflare R2. Canary\nreleases currently publish to R2 only. See\n[workspace-server packaging](apps/workspace-server/docs/packaging.md) for its independent release\nand channel rules.\n\n## Code Style & Conventions\n\n- Use `pnpm`; do not introduce npm or yarn lockfile churn.\n- Format with `oxfmt`; config is `.oxfmtrc.json`.\n- Keep formatted lines near the configured `printWidth` of 100 characters.\n- Use 2 spaces, semicolons, single quotes in TS, double quotes in JSX, LF endings,\n  trailing commas where valid in ES5, and sorted imports.\n- Lint with `oxlint`; config is `.oxlintrc.json` with correctness, TypeScript, React\n  hooks, and local repo rules enabled.\n- TypeScript strict mode is enabled; app targets share `apps/emdash-desktop/tsconfig.json`.\n- Avoid `any`; if a registry or boundary needs it, keep the escape local and documented.\n- Use top-level `import` statements; do not use `require()`.\n- Never re-export as a shortcut; import from the original source.\n- Components use `PascalCase`; hooks use `useX` camelCase or an existing local pattern.\n- Tests use `*.test.ts` or `*.test.tsx`.\n- Slice Wire controllers live in `src/core/features/<feature>/node/` and are aggregated by\n  `src/core/manifests/node/controllers.ts`; they delegate to imported operation or service\n  functions.\n- Renderer-main calls go through Wire via the seeded connection seam in\n  `src/core/primitives/wire/browser/connection.ts`: the renderer bootstrap seeds it once at\n  startup, and each slice exposes a typed domain client from its `api/` (for example\n  `getMcpClient()`) built on `domainClient`.\n- Feature UI lives under `src/core/features/<feature>/browser/`; the editor slice owns Monaco and\n  file rendering under `src/core/features/editor/browser/`; portable browser primitives (modals,\n  views, navigation, commands, scoped stores) live under `src/core/primitives/`; the thin\n  renderer host shell lives under `src/renderer/lib/`.\n- New feature modals and views must be exposed through the owning slice's `contributions/browser.ts`\n  and aggregated by `src/core/manifests/browser/browser-contributions.ts`.\n- New task tabs contribute providers through `src/core/features/<feature>/contributions/tabs.ts`,\n  aggregated by `src/core/manifests/browser/task-tab-contributions.ts`; the workbench registry\n  lives at `src/core/features/workbench/api/browser/task-tab-registry.ts`.\n- New commands are defined with `defineCommand` (`src/core/primitives/commands/`) in the owning\n  slice's `contributions/commands.ts`, aggregated by\n  `src/core/manifests/shared/command-catalog.ts`; view scopes\n  (`src/core/primitives/view-scopes/`) bind availability and implementation.\n- Commit messages should follow Conventional Commits:\n\n```text\n<type>(<scope>): <short imperative summary>\n\nExamples:\nfix(opencode): change initialPromptFlag from -p to --prompt for TUI\nfeat(docs): add changelog tab with GitHub releases integration\n```\n\n## Architecture Notes\n\n```mermaid\nflowchart LR\n  User[User] --> Renderer[React renderer]\n  Renderer --> Clients[Typed Wire domain clients]\n  Clients --> Seam[Seeded wire connection seam]\n  Seam --> Preload[Preload wire port]\n  Preload --> Main[Electron main process]\n  Main --> Gateway[Desktop Wire gateway]\n  Gateway --> Controllers[Slice Wire controllers]\n  Controllers --> Services[Domain services]\n  Services --> DB[(SQLite via Drizzle)]\n  Services --> Runtime[Runtime services]\n  Runtime --> PTY[PTY sessions]\n  Runtime --> ACP[ACP sessions]\n  Runtime --> SSH[SSH and SFTP]\n  Services --> VCS[Git, GitHub, GitLab, PRs]\n  Services --> Issues[Issue integrations]\n  Services --> MCP[MCP and skills]\n  ACP --> CoreAcp[@emdash/core ACP runtime]\n  ACP --> Plugins[@emdash/plugins providers]\n  Renderer --> ChatUI[@emdash/chat-ui]\n  Main --> Events[Live models and typed events]\n  Events --> Renderer\n```\n\nThe app boots from `src/entry/main.ts`, which runs the phased bootstrap in\n`src/main/bootstrap/` (environment, database, window creation, recovery). Renderer-main\ntraffic is served by the desktop Wire gateway (`src/main/gateway/desktop-wire.ts`) from the\ncontract assembled in `src/core/manifests/shared/desktop-wire-contract.ts` and the controllers\naggregated in `src/core/manifests/node/controllers.ts`. The preload bridge\n(`src/entry/preload.ts`) exposes only `requestWirePort` and `getPathForFile`. The renderer is\na React app that calls typed Wire domain clients, subscribes to live models and typed events,\nand coordinates views, tabs, modals, commands, project state, terminal state, and task\nworkflows.\n\nTask execution has two runtime paths. Legacy/TUI conversations run through PTY\nservices under `packages/core/src/services/pty/` and the terminal/TUI runtimes under\n`packages/core/src/runtimes/terminals/` and `packages/core/src/runtimes/tui-agents/`.\nStructured chat conversations use ACP: provider plugins in `packages/plugins/` expose\nACP behavior, `packages/core/src/runtimes/acp/` owns protocol/session state, terminal\nmanagement, and process hosting, the desktop launches that runtime as a Wire component worker\nfrom `src/main/gateway/entries/acp.ts` (`src/main/core/acp/` bridges agent status), and\n`src/core/features/conversations/browser/acp/` maps updates into `@emdash/chat-ui`.\n\nMain-process adapter domains live under `src/main/core/`: ACP, agent status, app,\ndependencies, file search, files, Git, preview servers, provider accounts, runtime,\nshared, terminal shell, and utils. Portable domain logic lives in vertical slices\nunder `src/core/` and in `packages/core/` (for example PTY services under\n`packages/core/src/services/pty/` and resource monitoring under\n`packages/core/src/runtimes/resource-usage/`). Expected failures should use the\n`Result<T, E>` pattern from `@emdash/shared` or the app-local result helpers.\n\n## Testing Strategy\n\nLocal merge gate:\n\n```bash\npnpm run format\npnpm run lint\npnpm run typecheck\npnpm run test\n```\n\n- Root `pnpm run test` uses Nx to run every workspace package test target.\n- App tests run with Vitest projects in `apps/emdash-desktop/vitest.config.ts`.\n- App `node` tests cover `src/**/*.test.ts` except DB, migration, and browser tests.\n- App `main-db` tests cover main-process integration tests that need real SQLite.\n- App `fixtures` tests generate DB fixtures via `pnpm run db:fixtures`.\n- App `migrations` tests validate Drizzle migrations via `pnpm run test:migrations`.\n- App `scripts` tests cover release and support scripts under `scripts/**/*.test.ts`.\n- App `browser` tests use Playwright-backed `@vitest/browser` for renderer behavior.\n- `packages/core` has ACP, dependency, plugin helper, Git, FS, and runtime unit tests.\n- `packages/chat-ui` has node, browser, perf, and benchmark test targets.\n- `packages/ui`, `packages/shared`, and `packages/plugins` run their package-local tests.\n- Integration-style tests create temporary repos and worktrees in `os.tmpdir()`.\n- CI runs `.github/workflows/code-consistency-check.yml` with `nx affected` for\n  `format:check`, `typecheck`, `lint`, and `test` on touched projects and\n  dependents; the Playwright-backed `browser` Vitest projects are skipped there.\n- Tests are still expected locally before merge even where CI coverage is narrower.\n\n## Security & Compliance\n\n- The project is licensed under Apache-2.0; see `LICENSE.md`.\n- Do not commit secrets, tokens, private keys, app databases, logs, build artifacts,\n  generated dependency folders, or release artifacts.\n- Application secrets are stored through encrypted app secret services and Electron\n  safe storage; SSH credentials are managed through SSH services and OS-backed storage.\n- Release secrets live in GitHub Actions secrets, including PostHog, Cloudflare R2,\n  Apple signing/notarization, Azure Trusted Signing, and Cachix credentials.\n- Telemetry must remain optional; users can disable it with `TELEMETRY_ENABLED=false`\n  or in app settings.\n- File logging must preserve redaction of common secret patterns.\n- PTY environment construction must go through `packages/core/src/services/pty/api/terminal-env.ts`.\n- Treat ACP process spawning, SSH command construction, shell escaping, PTY spawning,\n  and worktree paths as security-sensitive.\n- Do not bypass path-safety, shell escaping, or validation helpers.\n- Use `pnpm-lock.yaml` for dependency integrity and review dependency changes.\n- This checkout does not define repo-local Dependabot config, CODEOWNERS, or SECURITY.md;\n  do not assume extra repository-owned security automation beyond the workflows present\n  in `.github/workflows/`.\n- Dependency changes must keep `pnpm-lock.yaml` in sync, preserve `packageManager` and\n  `pnpm.onlyBuiltDependencies`, and avoid introducing new install scripts or native\n  builds without explicit review.\n- CI installs with `pnpm install --frozen-lockfile --ignore-scripts` in\n  `.github/workflows/code-consistency-check.yml`; changes that rely on install-time\n  side effects need clear justification.\n- Prefer existing dependencies and workspace packages over adding new third-party\n  packages. When adding a dependency, document why the existing stack is insufficient\n  and check license/security posture before committing the lockfile change.\n\n## Agent Guardrails\n\n- Start with this file for repo-wide context and required commands.\n- Load only the relevant `agents/` topic page for the area you are changing.\n- Prefer updating the smallest applicable `agents/` page over expanding this file.\n- If nested `AGENTS.md` files are added later, the closest file to the edited path wins.\n- Explicit user or maintainer instructions override this file.\n- Do not hand-edit numbered Drizzle migrations or `drizzle/meta/`.\n- Use `pnpm run db:generate` for new migrations, then update fixtures and migration tests.\n- Avoid editing `dist/`, `release/`, `out/`, `build/`, and generated package artifacts\n  unless the task is explicitly about packaging, signing, or release behavior.\n- Do not dispatch release workflows, publish packages, upload artifacts, or trigger\n  external deployments unless the user explicitly asks for release work.\n- Treat `src/main/core/acp/`, `packages/core/src/services/pty/`,\n  `src/core/services/ssh/`, `src/main/db/`, updater code, and provider process\n  spawning as high risk.\n- Read the matching `agents/risky-areas/` page before touching database, PTY, SSH, or\n  updater code.\n- Do not weaken shell quoting, spawn behavior, env allowlists, path validation, or\n  secret redaction casually.\n- Prefer existing service, provider, plugin, Wire, modal, view, tab, and store patterns\n  over new abstractions.\n- New Wire procedures belong in the owning slice's `api/` contract with a controller in\n  `node/`, registered through `src/core/manifests/shared/desktop-wire-contract.ts` and\n  `src/core/manifests/node/controllers.ts`.\n- Keep renderer-main calls on typed Wire contracts, live models, and typed events. The preload\n  bridge should stay small; add direct `window.electronAPI` surface only when an\n  Electron/browser primitive cannot fit the Wire path.\n- Access task and Project MobX stores through selectors and task view hooks:\n  `getTaskStore`, `asProvisioned`, `taskViewKind`, `getTaskManagerStore`,\n  `getProjectStore`, `asAvailableProject`, `getProjectHostAccess`, `useTaskViewKind`, `useWorkspace`,\n  `useWorkspaceId`, `useDevServers`, `useWorkspaceViewModel`, `useConversations`,\n  and `useTerminals`.\n- Never use `asProvisioned(...)!` or `asAvailableProject(...)!`; use explicit null checks.\n- State guards must check `kind !== 'ready'` rather than enumerating non-ready states.\n- Access task managers through `getTaskManagerStore(projectId)`, not `project.taskManager`.\n- Access available Project contexts through `asAvailableProject(getProjectStore(id))`, not inline\n  guards. Read Host-dependent action state through that context's `host` access interface.\n- Task selectors live in `src/core/features/tasks/api/browser/task-state/task-selectors.ts`.\n- Project selectors live in `src/core/features/projects/api/browser/stores/project-selectors.ts`.\n- For provider changes, update plugin metadata, shared provider metadata, ACP support\n  flags, PTY env passthrough if needed, hook integrations, renderer assumptions, and\n  tests for non-standard behavior.\n- For ACP changes, preserve protocol state-machine behavior in `packages/core/src/runtimes/acp/`,\n  keep provider-specific transforms in `packages/plugins/`, and adapt UI payloads at\n  app or chat-UI edges.\n- For MCP changes, keep canonical data in shared types and adapt provider formats at edges.\n- Follow `.github/PULL_REQUEST_TEMPLATE.md`: keep PRs small and focused, self-review\n  before handoff, list checks run, attach UI evidence when applicable, and update docs\n  and tests when behavior changes.\n- Call out high-risk changes explicitly in the PR description or handoff notes,\n  especially database, ACP, PTY, SSH, updater, provider spawning, dependency, and\n  release-related changes.\n- Do not self-approve, merge, assign reviewers, edit branch protection, or change\n  workflow permissions unless the user explicitly asks.\n- Avoid scripted loops against GitHub, Linear, Jira, GitLab, provider CLIs, or release\n  workflows. Use focused queries and respect existing workflow retries; ask before\n  adding polling, bulk API calls, or scheduled automation.\n- Keep automation scoped to the task. Do not run the full local merge gate repeatedly\n  when a focused check is enough during iteration; run broader checks before handoff\n  when the change scope justifies it.\n\n## Extensibility Hooks\n\n- Agent provider plugins live in `packages/plugins/src/agents/impl/` and are registered\n  in `packages/plugins/src/agents/registry.ts`.\n- Provider plugin hosting services live in `packages/core/src/services/agent-plugins/`.\n- Agent provider metadata and capabilities live in `packages/plugins/src/agents/registry.ts`\n  and `packages/plugins/src/agents/impl/`; renderer-facing DTOs are built by\n  `src/core/features/agents/node/agent-payload-builder.ts`.\n- ACP support is exposed through plugin ACP capabilities and portable Core ACP APIs.\n- Provider detection lives in `src/main/core/dependencies/`.\n- Provider PTY behavior and env construction live under `packages/core/src/services/pty/`.\n- Provider event hooks and plugins are installed and hosted by\n `packages/core/src/runtimes/tui-agents/`; desktop projection lives under\n `src/main/core/agent-status/`.\n- ACP process hosting lives in `packages/core/src/runtimes/acp/`; the desktop launches the\n  runtime as a Wire component worker from `src/main/gateway/entries/acp.ts`.\n- Feature modal and view definitions are contributed by slices and aggregated in\n  `src/core/manifests/browser/browser-contributions.ts`; renderer app registries compose host entries.\n- Task tab providers are aggregated through\n  `src/core/manifests/browser/task-tab-contributions.ts`.\n- MCP server config handling lives in `src/core/features/mcp/node/` (wire controller and\n  registration utils), canonical types in `src/core/primitives/mcp/api/`, and UI in\n  `src/core/features/mcp/browser/`.\n- Skills types and validation live in Core primitives; skills UI and service code live in\n  `src/core/features/skills/browser/` and `src/core/features/skills/node/`.\n- Team-owned runtime settings live in each workspace's `.emdash.json`:\n  `preservePatterns`, `scripts.prepare`, `scripts.setup`, `scripts.run`,\n  `scripts.teardown`, and `shellSetup` (a per-workspace override of the host default).\n- Project overrides such as `worktreeRoot`, `defaultBranch`, `baseRemote`, `pushRemote`,\n  `githubAccount`, `agentGitCredentials`, and `tmux` are DB-backed, not `.emdash.json`. Effective\n  tmux resolves through project override > host default > app default; it is not seeded from a\n  default at project creation.\n- The workspace registry owns lifecycle and file-handling config. Host-local personal config\n  (`preservePatterns`, scripts, and auto-run toggles) is versioned JSON on the repository or\n  directory project-root record; worktrees resolve against their project root's personal config.\n- Read registry-owned config through the keyed `projectConfig` live model or `getProjectConfig`.\n  It exposes raw personal/team layers, team-file sources, and one `{ value, from }` result per\n  field. Applicable precedence is personal > that workspace's `.emdash.json` > host default >\n  built-in; arrays replace rather than merge.\n- Per-host defaults (`shellSetup`, `worktreeRoot`, `tmux`) live in the host-settings\n  runtime (`packages/core/src/runtimes/host-settings/`), stored as JSON in the host's\n  emdash data directory and editable from the machines/system settings UI.\n- Desktop project settings compose self-contained domain snapshots: the registry owns\n  lifecycle/file handling, while desktop DB and host-settings domains own Git identity and\n  placement. Forms edit raw layers with explicit per-domain patches; there is no merged settings\n  bag to persist.\n- The scripts runtime is a strict executor: callers must provide resolved `command` and\n  `shellSetup`; it never reads config files or host defaults. The workspace registry resolves and\n  sequences lifecycle commands, then observes runs into durable lifecycle steps.\n- Desktop settings migrations are centralized under\n  `src/core/features/projects/node/settings/migrations/` and run in order when an attachment is\n  established. Legacy schemas and readers stay migration-only; destination markers make imports\n  idempotent and retryable.\n- Optional environment variables include `TELEMETRY_ENABLED`, `EMDASH_DB_FILE`,\n  `EMDASH_DISABLE_NATIVE_DB`, `EMDASH_DISABLE_PTY`, `EMDASH_AUTH_SERVER_URL` (development\n  builds only), `CODEX_SANDBOX_MODE`, and `CODEX_APPROVAL_POLICY`.\n- Build-time telemetry configuration may use `VITE_POSTHOG_KEY` and `VITE_POSTHOG_HOST`.\n- Runtime feature flags are read through telemetry-backed feature flag helpers.\n- App-internal path aliases are defined in `tsconfig.json` (`@/*`, `@core/*`, `@renderer/*`,\n  `@main/*`, `@root/*`, and `@tooling/*`) and mirrored where needed in\n  `electron.vite.config.ts`; workspace packages resolve through their `exports` maps with a\n  `development` condition, not aliases.\n- Versioned JSON column schemas use `defineVersionedSchema()` from\n  `@emdash/core/primitives/versioned-schema/api`\n (`packages/core/src/primitives/versioned-schema/api/versioned-schema.ts`) and Drizzle\n `versionedJsonColumn()` from `src/core/services/app-db/node/versioned-column.ts`.\n\n## Further Reading\n\n- [Agent docs map](agents/README.md)\n- [Quickstart](agents/quickstart.md)\n- [Architecture overview](agents/architecture/overview.md)\n- [Settings ownership and precedence](agents/architecture/settings.md)\n- [Main process architecture](agents/architecture/main-process.md)\n- [Renderer architecture](agents/architecture/renderer.md)\n- [Shared modules](agents/architecture/shared.md)\n- [Developer flows](agents/workflows/flows.md)\n- [Nx task orchestration and caching](agents/workflows/nx.md)\n- [Testing workflow](agents/workflows/testing.md)\n- [Worktrees workflow](agents/workflows/worktrees.md)\n- [Remote development workflow](agents/workflows/remote-development.md)\n- [Workspace server architecture](agents/architecture/workspace-server.md)\n- [Provider integration](agents/integrations/providers.md)\n- [MCP integration](agents/integrations/mcp.md)\n- [IPC conventions](agents/conventions/ipc.md)\n- [Main-process patterns](agents/conventions/main-patterns.md)\n- [Renderer patterns](agents/conventions/renderer-patterns.md)\n- [TypeScript and React conventions](agents/conventions/typescript.md)\n- [Config file rules](agents/conventions/config-files.md)\n- [UI kit conventions](agents/conventions/ui-kit.md)\n- [UI styling conventions](agents/conventions/ui-styling.md)\n- [Versioned schema conventions](agents/conventions/versioned-schemas.md)\n- [Database risk notes](agents/risky-areas/database.md)\n- [PTY risk notes](agents/risky-areas/pty.md)\n- [SSH risk notes](agents/risky-areas/ssh.md)\n- [Updater risk notes](agents/risky-areas/updater.md)\n- [Contributing guide](CONTRIBUTING.md)\n- [Project README](README.md)\n","CLAUDE.md":"@AGENTS.md\n","agents/README.md":"# Agent Docs\n\nThis directory is the system of record for agent-facing repo guidance. Keep topic pages small, specific, and mechanically checkable where possible.\n\n## Recommended Reading Order\n\n1. `quickstart.md`\n2. `architecture/overview.md`\n3. the task-specific page for the area you are changing\n\n## Directory Layout\n\n- `architecture/`\n  - system structure and major code ownership boundaries\n  - [`core-modules.md`](architecture/core-modules.md) — Core runtime, service, and primitive module types, platform surfaces, dependency rules, and subpath exports\n  - [`workspace-server.md`](architecture/workspace-server.md) — protocol versioning, negotiation handshake, and behavior-change rules for the remote workspace daemon\n  - [`state-ownership.md`](architecture/state-ownership.md) — desktop-owned entities, host-owned resources, bindings, and the placement rule for operation behavior\n  - [`acp-runtime.md`](architecture/acp-runtime.md) — ACP runtime, session manager, connection pool, cells, live models, and API contract ownership\n  - [`git-runtime.md`](architecture/git-runtime.md) — Git contract/runtime ownership, session lifecycle, and nested wire composition\n  - [`path-system.md`](architecture/path-system.md) — host-aware path identity, resource URI ownership, and future migration boundaries\n- `workflows/`\n  - task-oriented procedures like testing, worktrees, remote development, and Nx task orchestration\n- `integrations/`\n  - provider, MCP, and external service guidance\n  - [`integration-plugins.md`](integrations/integration-plugins.md) — integration auth and credential contracts, stable account identity, issue-, pull-request-, and repository-plugin contracts, remote-to-integration routing\n- `risky-areas/`\n  - places where incorrect changes are expensive\n- `conventions/`\n  - coding contracts and repo rules\n  - [`ui-kit.md`](conventions/ui-kit.md) — which UI kit to use: `@emdash/ui` for components, feature Tailwind for layout one-offs, where new primitives go, `cn()` location, theming\n  - [`ui-styling.md`](conventions/ui-styling.md) — vanilla-extract styling rules for `packages/ui`\n\n## Maintenance Rules\n\n- Prefer one page per concrete topic.\n- Avoid volatile counts unless you can verify them cheaply.\n- Link to the source-of-truth file paths.\n- Update the smallest relevant page instead of expanding `AGENTS.md`.\n","agents/architecture/path-system.md":"# Path System\n\n`@emdash/core/primitives/path/api` is the source of truth for portable file identity and lexical\npath operations. The detailed package docs live in\n[`packages/core/docs/path/README.md`](../../packages/core/docs/path/README.md).\n\n## Ownership\n\n- `packages/core/src/path/` owns pure structured path primitives, resource URI\n  encoding, and comparison keys.\n- Desktop placement resolution owns repository and worktree location policy. It combines the target\n  host's home directory from `files.getHomeDir`, desktop settings, per-project overrides, and UI\n  input, then sends absolute paths to the host runtime.\n- Repository destinations default to `~/emdash/repositories/<name>` with numeric suffix allocation.\n  Worktree pools default to `~/emdash/worktrees/<repo-basename>-<hash8(repo-path)>`; the workspace\n  planner adds the sanitized branch leaf.\n- Runtime and app layers own host resolution, native string conversion,\n  filesystem I/O, realpath checks, authorization, and persistence migrations.\n- Renderer code should eventually prefer `ResourceUri` or structured refs over\n  raw absolute strings, but existing consumers are not migrated by the foundation\n  module.\n\n## Representation Rules\n\n- Use `HostFileRef` for a full file address on a host.\n- Use `ScopedPath` for root-relative operations such as Git paths, watcher\n  events, tree entries, and bulk calls.\n- Use `ResourceUri` for serialized identity.\n- Use `ResourceKey` for in-memory dedupe and maps.\n- Use explicit `PathProfile` semantics instead of `node:path` behavior when\n  interpreting cross-host paths.\n\n## Future Adoption Map\n\n- Core `files` path helpers can delegate lexical operations to\n  `@emdash/core/primitives/path/api`.\n- Desktop `RuntimePath` and SSH path helpers can become adapters around\n  structured path parsing/formatting.\n- Git models can represent repo-relative file coordinates as\n  `PortableRelativePath`.\n- fs-watch can normalize native events into `ScopedPath` before batching and\n  coalescing.\n- workspace-server schemas can move from documented string conventions to\n  structured path schemas.\n- Renderer tabs, tree nodes, Monaco models, comments, and view-state snapshots\n  can migrate from raw path strings to `ResourceUri`.\n\nDo not add filesystem I/O, `realpath()`, host registry state, or app-specific\nauthorization to `packages/core/src/path/`.\n","apps/emdash-desktop/src/core/primitives/commands/README.md":"# Commands\n\nCommands are portable, schema-backed action definitions. Definitions contain\nstatic metadata and optional keybindings; view scopes provide their runtime\navailability and implementation.\n\n```ts\nconst createBranchCommand = defineCommand({\n  id: 'task.createBranch',\n  title: 'Create Branch',\n  category: 'Git',\n  input: z.object({ branchName: z.string(), baseRef: z.string() }),\n});\n```\n\nCommand input describes arguments supplied for one invocation. Scope params\ndescribe the context in which the command runs; callers must not duplicate\nscope context in command input.\n","packages/core/README.md":"# @emdash/core\n\n`@emdash/core` owns Emdash's shared domain APIs and the host-scoped runtime and\nservice implementations used by the desktop app and workspace-server. It is the\npackage for code that must be shared across hosts without putting that ownership\nin an app package.\n\nCore keeps domain definitions close to their implementations, but separates\nportable APIs from platform-specific code through explicit subpath exports. A\nconsumer should be able to tell from the import path whether it is importing\nshared vocabulary, a Node implementation, or a browser implementation.\n\n## Package Structure\n\nSource modules live under `src/` and are grouped by module type:\n\n```text\nsrc/\n  primitives/\n  services/\n  runtimes/\n  workspace-server/\n```\n\n### Primitives\n\n`primitives/` contains reusable vocabulary and small infrastructure that does\nnot own a product domain. Primitives may define schemas, value objects,\ndeterministic helpers, and narrowly scoped policies.\n\nExamples include host-aware paths, host identity, skill schemas, MCP schemas,\nagent environment helpers, plugin file-system ports, and concurrency helpers.\n\nPrimitives may depend on other primitives, but they must not depend on services\nor runtimes.\n\n### Services\n\n`services/` contains focused capabilities that can be injected into runtimes or\nhost composition code. Services may own bounded resources, expose ports, provide\nNode or browser implementations, or define Wire contracts for smaller shared\ncapabilities.\n\nExamples include execution contexts, PTY management, filesystem watching, host\ndependency detection, and agent plugin support.\n\nServices may depend on primitives and explicitly lower-level services. They must\nnot depend on runtimes.\n\n### Runtimes\n\n`runtimes/` contains host-scoped composition roots for larger product domains.\nA runtime typically implements a public Wire contract, owns domain state and\nresources, and coordinates injected services and primitives.\n\nCurrent runtime domains include:\n\n- `acp`\n- `agent-config`\n- `files`\n- `git`\n- `tui-agents`\n- `workspace`\n\nRuntimes are peers. A runtime must not import another runtime; cross-runtime\ncomposition belongs in the desktop host, workspace-server host, or another\nexplicit composition root.\n\n### Workspace Server\n\n`workspace-server/` is a special composition root, not a fourth general module\ntype. It owns workspace-server protocol versions, shared host schemas, aggregate\nWire contracts, and host-owned composition helpers. It may compose runtime APIs,\nservices, and primitives.\n\n## Surface Conventions\n\nEach module is split by platform surface:\n\n```text\n<module-type>/<module-name>/\n  api/\n  node/\n  browser/\n```\n\nModules only add the surfaces they need.\n\n### `api`\n\n`api/` is the portable public surface. It may contain Wire contracts, serialized\nmodels, DTOs, Zod schemas, public ports, typed errors, and pure helpers needed by\nconsumers.\n\n`api/` must not import `node/` or `browser/`, use platform modules, spawn\nprocesses, or own background work.\n\n### `node`\n\n`node/` contains Node-specific implementations: runtime and service factories,\nWire components, Wire controllers, procedure implementations, filesystem/Git/PTY/subprocess\nbehavior, and resource allocation. App-owned worker entry files adapt components to process hosting\nwith `runWireComponentWorker()`.\n\n### `browser`\n\n`browser/` contains browser-specific implementations of portable APIs. Browser\nsurfaces may import `api/` and other `browser/` surfaces, but must not import\nNode code.\n\n## Subpath Exports\n\nPublic imports must select an explicit module type, module name, and surface:\n\n```text\n@emdash/core/<module-type>/<module-name>/<surface>\n```\n\nExamples:\n\n```ts\nimport { gitContract } from '@emdash/core/runtimes/git/api';\nimport { GitRuntime, gitComponent } from '@emdash/core/runtimes/git/node';\n\nimport type { IWatchService } from '@emdash/core/services/fs-watch/api';\nimport { createNativeWatchService } from '@emdash/core/services/fs-watch/node';\n\nimport { parseAbsolute, type HostAbsolutePath } from '@emdash/core/primitives/path/api';\n```\n\nAvoid ambiguous module-root exports such as `@emdash/core/runtimes/git`.\nConsumers should choose `api`, `node`, or `browser` explicitly.\n\nInside `packages/core/src`, package subpath imports (the `imports` field in\n`package.json`) are used for cross-directory imports:\n\n```ts\nimport { parseAbsolute } from '#primitives/path/api';\nimport { createNativeWatchService } from '#services/fs-watch/node';\nimport { gitContract } from '#runtimes/git/api';\n```\n\nThose subpath imports are internal to Core source and resolve through Core's\nown `package.json`, so consumers need no extra configuration. Code outside\nCore should import through public `@emdash/core/...` subpaths.\n\n## More Details\n\nSee [`../../agents/architecture/core-modules.md`](../../agents/architecture/core-modules.md)\nfor the full architecture notes, dependency rules, and migration guidance.\n","packages/core/docs/README.md":"# @emdash/core Docs\n\n`@emdash/core` contains transport-agnostic primitives shared by the desktop app,\nworkspace server, worker processes, and provider runtimes.\n\n## Public Exports\n\n- [`@emdash/core/primitives/path/api`](path/README.md) - pure host-aware path identity,\n  normalization, resource URI, and comparison helpers.\n\n## Boundaries\n\nCore package docs describe reusable primitives. App-specific wiring, persistence\nmigrations, Electron IPC, and host runtime ownership stay in the app or workspace\nserver packages that own those concerns.\n","packages/core/docs/path/README.md":"# Path System\n\n`@emdash/core/primitives/path/api` defines Emdash's portable file identity vocabulary. It is\npure TypeScript, browser-safe, and does not perform filesystem I/O.\n\nUse this module when a path crosses a process, renderer, persistence, or Wire\nboundary. Runtime adapters remain responsible for converting structured paths\ninto native strings immediately before filesystem, Git, watcher, or process\nspawn operations.\n\n## Concepts\n\n- `HostRef` from `@emdash/core/primitives/host/api` selects a local or remote runtime host.\n- `HostAbsolutePath` is a structured absolute path with an explicit root:\n  POSIX root, Windows drive, or UNC share.\n- `PortableRelativePath` is a normalized `/`-separated coordinate relative to a\n  known root. The empty string represents the root.\n- `HostFileRef` combines `HostRef` and `HostAbsolutePath` into a routeable file\n  address.\n- `ScopedPath` combines a root `HostFileRef` and `PortableRelativePath`.\n- `ResourceUri` is the versioned string form for serialization and persistence.\n- `ResourceKey` is an opaque comparison key for maps and deduplication.\n\n## Basic Flow\n\n```ts\nimport {\n  encodeResourceUri,\n  hostFileRef,\n  parseAbsolute,\n  parsePortableRelativePath,\n  resolveScopedPath,\n  scopedPath,\n} from '@emdash/core/primitives/path/api';\nimport { LOCAL_HOST_REF } from '@emdash/core/primitives/host/api';\n\nconst rootPath = parseAbsolute('/repo', { profile: { style: 'posix' } });\nconst relative = parsePortableRelativePath('src/index.ts');\n\nif (rootPath.success && relative.success) {\n  const root = hostFileRef(LOCAL_HOST_REF, rootPath.data);\n  const scoped = scopedPath(root, relative.data);\n  const resolved = resolveScopedPath(scoped);\n\n  if (resolved.success) {\n    const uri = encodeResourceUri(resolved.data);\n    // emdash-file://v2/local/local/posix/repo/src/index.ts\n  }\n}\n```\n\n`HostFileRef` belongs at routing, persistence, and renderer identity boundaries.\nOnce a domain controller has selected a host-bound Wire connection, Git and Files\nruntime contracts carry `HostAbsolutePath` roots and `PortableRelativePath`\ncoordinates without repeating `HostRef` in every request.\n\n## Related Pages\n\n- [Normalization](normalization.md)\n- [Resources](resources.md)\n- [Boundaries](boundaries.md)\n- [Recipes](recipes.md)\n","packages/core/docs/path/boundaries.md":"# Boundaries\n\nThe path module owns pure identity and lexical path math. It does not own host\nlifecycle, filesystem I/O, authorization, or persistence migrations.\n\n## Conversion Points\n\n```mermaid\nflowchart LR\n  NativeInput[Native string] --> Parser[Path parser]\n  Parser --> Ref[HostFileRef]\n  Ref --> Controller[Domain controller]\n  Controller -->|select by HostRef| Wire[Host-bound Wire connection]\n  Wire --> Runtime[Runtime path key]\n  Runtime --> NativeOutput[Native string]\n  NativeOutput --> Fs[Filesystem or Git]\n```\n\nConvert at these boundaries:\n\n- User input, picker output, SSH responses, and Git roots become structured refs\n  at ingress.\n- Renderer and persistence payloads use `ResourceUri`, `HostFileRef`, or\n  `ScopedPath`, not raw native strings.\n- Domain controllers use `HostRef` to select or relay to a host-bound Wire\n  connection. They do not inject host routing identity into the runtime.\n- Host-bound Wire payloads use `HostAbsolutePath` roots and\n  `PortableRelativePath` coordinates.\n- Host runtimes convert structured paths back to native strings immediately before\n  filesystem, Git, watcher, or process-spawn calls.\n- Watcher and Git events should become `ScopedPath` values before crossing to\n  renderer-facing models.\n\n## Contract Validation\n\nContracts should import schemas from `@emdash/core/path` instead of introducing\nnew `z.string()` path fields once they migrate to structured resources.\n\nRecommended boundary shapes:\n\n- `hostFileRefSchema` for one-off global file resources.\n- `scopedPathSchema` for a single file under a known root.\n- `{ root: hostAbsolutePathSchema, path: portableRelativePathSchema }` for\n  host-bound runtime APIs, tree keys, watcher events, and Git paths.\n- `{ root: hostFileRefSchema, path: portableRelativePathSchema }` while a payload\n  still needs host routing.\n- `resourceUriSchema` for persisted string identity.\n- `absolutePathInputSchema(profile)` for native/user/host input only.\n\nThe schemas delegate to the same parsers as internal code, so contract\nvalidation and local validation enforce the same invariants.\n\n## Lexical Versus Physical Containment\n\n`containsAbsolute()` and `relativizeHostFileRef()` are lexical. They do not\nfollow symlinks.\n\nUse lexical containment for identity, tree projection, deduplication, batching,\nand root-relative path conversion.\n\nUse realpath-based containment for security-sensitive writes, deletes, and\nfilesystem mutation policies. That logic belongs in runtime or app services with\naccess to a filesystem implementation.\n\n## Not Owned Here\n\nThis module intentionally does not implement:\n\n- host connection ownership or mapping from `HostRef` to a live runtime;\n- SSH connection management;\n- workspace mount persistence;\n- `realpath()` or canonical inode checks;\n- renderer tab migrations;\n- Monaco model migration;\n- Wire contract migrations;\n- filesystem authorization.\n\n## Future Adoption Map\n\nExisting consumers can migrate incrementally:\n\n- Files contracts use `HostAbsolutePath` roots and `PortableRelativePath`\n  coordinates; runtime policy adds realpath-based mutation safety.\n- Desktop `RuntimePath` can become a thin native-format adapter around\n  `PathSemantics`.\n- SSH path helpers can use POSIX parsing explicitly instead of app-local string\n  utilities.\n- Git models use repository/worktree `HostAbsolutePath` roots and keep file paths\n  as `PortableRelativePath`.\n- fs-watch can emit root-scoped portable paths, then batch using `ResourceKey`.\n- workspace-server schemas can validate structured refs instead of documenting\n  string conventions only.\n- Renderer tabs, tree nodes, Monaco models, comments, and view-state snapshots\n  can store `ResourceUri` while deriving display paths from workspace mounts.\n","packages/core/docs/path/normalization.md":"# Normalization\n\nPath normalization in Emdash is explicit. Callers choose the dialect they are\nparsing instead of inheriting the operating system of the current process.\n\n## Native Absolute Paths\n\n`parseAbsolute()` accepts a `profile` option.\n\n```ts\nparseAbsolute('/home/david/repo', { profile: { style: 'posix' } });\nparseAbsolute('C:\\\\Users\\\\David\\\\repo', { profile: { style: 'win32' } });\nparseAbsolute('\\\\\\\\server\\\\share\\\\repo', { profile: { style: 'win32' } });\n```\n\nPOSIX parsing treats backslash as a valid filename character. Windows parsing\ntreats both `\\\\` and `/` as separators.\n\nThe parser:\n\n- rejects null bytes;\n- resolves `.` and `..` lexically;\n- rejects paths that escape their root;\n- preserves Unicode spelling by default and normalizes only when requested;\n- preserves path spelling and casing for display;\n- uppercases Windows drive letters in the structured root.\n\nThe parser does not:\n\n- call `realpath()`;\n- check whether the path exists;\n- authorize access;\n- turn a POSIX backslash into a separator.\n\n## Portable Relative Paths\n\n`PortableRelativePath` is used for scoped coordinates such as file tree keys,\nGit paths, watcher events, and root-relative Wire payloads.\n\nRules:\n\n- `/` is the separator.\n- The empty string is the root.\n- Leading `/`, Windows drive roots, and UNC roots are rejected.\n- `.` and `..` are resolved lexically; escaping the root is rejected.\n- Backslash is a literal character, not a separator.\n- Null bytes are rejected.\n\n```ts\nparsePortableRelativePath('src/./components/../index.ts');\n// => 'src/index.ts'\n\nparsePortableRelativePath('../outside');\n// => invalid-path\n```\n\n## Comparison\n\nStored paths preserve spelling. Equality and deduplication use comparison keys\ncreated from an explicit `PathProfile`.\n\n```ts\nconst semantics = createPathSemantics({\n  style: 'win32',\n  caseSensitivity: 'insensitive',\n  unicodeNormalization: 'nfc',\n});\n```\n\nDo not lowercase or otherwise rewrite stored paths to make comparison easier.\nUse `ResourceKey` or `PathSemantics.comparisonKey()` for maps and sets.\n","packages/core/docs/path/recipes.md":"# Recipes\n\n## Parse a Native POSIX Path\n\n```ts\nconst parsed = parseAbsolute('/home/david/repo/src/index.ts', {\n  profile: { style: 'posix' },\n});\n```\n\n## Parse a Native Windows Path\n\n```ts\nconst parsed = parseAbsolute('C:\\\\Users\\\\David\\\\repo\\\\src\\\\index.ts', {\n  profile: { style: 'win32' },\n});\n```\n\n## Build a Host File Ref\n\n```ts\nconst host = hostRef('remote', 'connection-1');\nconst path = parseAbsolute('/repo/src/index.ts', { profile: { style: 'posix' } });\n\nif (path.success) {\n  const ref = hostFileRef(host, path.data);\n}\n```\n\n## Use a Root-Scoped Path\n\n```ts\nconst root = hostFileRef(LOCAL_HOST_REF, rootPath);\nconst relative = parsePortableRelativePath('src/index.ts');\n\nif (relative.success) {\n  const scoped = scopedPath(root, relative.data);\n  const resolved = resolveScopedPath(scoped);\n}\n```\n\nUse this pattern for watcher events, Git paths, file tree nodes, and bulk file\noperations.\n\n## Store a Resource URI\n\n```ts\nconst uri = encodeResourceUri(ref);\nconst decoded = decodeResourceUri(uri);\n```\n\nStore the URI when a durable string identity is needed. Keep `ResourceKey` for\nin-memory maps only.\n\n## Deduplicate with a Resource Key\n\n```ts\nconst key = resourceKeyFromFileRef(ref, {\n  profile: {\n    style: 'win32',\n    caseSensitivity: 'insensitive',\n    unicodeNormalization: 'nfc',\n  },\n});\n\nmodelsByResource.set(key, model);\n```\n\n## Convert an Event Under a Watched Root\n\n```ts\nconst changed = hostFileRef(root.host, changedAbsolutePath);\nconst relative = relativizeHostFileRef(root, changed);\n\nif (relative.success) {\n  const scoped = scopedPath(root, relative.data);\n}\n```\n\nBatch and coalesce watcher events with resource keys derived from the resolved\nfile refs, not by string-prefix comparisons.\n\n## Validate a Contract Model\n\nUse structured schemas for contracts instead of ad hoc `z.string()` path fields.\n\n```ts\nconst readTextInputSchema = z.object({\n  file: scopedPathSchema,\n  options: readFileOptionsSchema.optional(),\n});\n```\n\nFor root-scoped bulk APIs, keeping root and relative path separate is often more\ncompact:\n\n```ts\nconst treePathKeySchema = z.object({\n  root: hostAbsolutePathSchema,\n  path: portableRelativePathSchema,\n  sessionId: z.string(),\n});\n```\n\nThis compact form is appropriate after a domain controller has already selected\nthe host-bound runtime connection. Use `hostFileRefSchema` instead when the same\npayload must still be routed to a host.\n\nFor durable UI state, use a resource URI:\n\n```ts\nconst fileTabStateSchema = z.object({\n  resource: resourceUriSchema,\n});\n```\n\nOnly use native absolute input schemas at ingress boundaries:\n\n```ts\nconst nativePickerInputSchema = z.object({\n  path: absolutePathInputSchema({ profile: { style: 'posix' } }),\n});\n```\n\n## Handle Errors\n\nAll parsing helpers return `Result<T, PathError>`.\n\n```ts\nconst parsed = parsePortableRelativePath(input);\nif (!parsed.success) {\n  switch (parsed.error.type) {\n    case 'invalid-path':\n      return parsed.error.message;\n    case 'outside-root':\n      return 'Path is outside the selected root';\n    default:\n      return 'Invalid resource';\n  }\n}\n```\n","packages/core/docs/path/resources.md":"# Resources\n\n`HostFileRef` is the canonical application-level file identity:\n\n```ts\ntype HostFileRef = {\n  host: HostRef;\n  path: HostAbsolutePath;\n};\n```\n\nIt identifies a logical path on a host. It is not an inode identity. Symlinks,\nhard links, and renames are handled by runtime-specific filesystem behavior.\n\n## Host References\n\n`HostRef` is defined by `@emdash/core/primitives/host/api`:\n\n```ts\ntype HostRef = {\n  type: 'local' | 'remote';\n  id: string;\n};\n```\n\nIt is used by domain controllers to select a host-bound runtime connection. A\nruntime executes on the addressed host and therefore does not need `HostRef` in\nits own Git or Files keys.\n\n## Absolute Roots\n\nStructured roots avoid ambiguity across hosts and drives:\n\n```ts\n{ kind: 'posix' }\n{ kind: 'drive', driveLetter: 'C' }\n{ kind: 'unc', server: 'server', share: 'share' }\n```\n\nThe path segments are stored separately from the root, so `/`, `C:/`, and\n`//server/share` cannot be confused.\n\n## Scoped Paths\n\nUse `ScopedPath` when many operations share a root:\n\n```ts\ntype ScopedPath = {\n  root: HostFileRef;\n  relative: PortableRelativePath;\n};\n```\n\nThis is the preferred globally addressable shape for watcher events, tree\nentries, and batch operations. Host-local runtime contracts use the equivalent\ncompact shape: a `HostAbsolutePath` root plus `PortableRelativePath` coordinates.\n\n## Resource URIs\n\n`ResourceUri` is the stable string encoding:\n\n```text\nemdash-file://v2/local/local/posix/home/david/repo/src/index.ts\nemdash-file://v2/remote/connection-1/drive/c/Users/David/repo/src/index.ts\nemdash-file://v2/remote/connection-2/unc/server/share/repo/src/index.ts\n```\n\nUse it for serialization, persistence, Monaco model identity, and durable\nmessages. Percent encoding is applied per path segment.\n\n## Contract Schemas\n\nPath schemas live next to the path primitives so Wire/RPC contracts and\npersisted models do not duplicate validation rules.\n\nUse:\n\n- `hostFileRefSchema` for a globally addressable file resource.\n- `scopedPathSchema` for a file under a known root.\n- `portableRelativePathSchema` for Git paths, file tree entry keys, and watcher\n  paths that are already scoped by another field.\n- `resourceUriSchema` for persisted or Monaco-style string identity.\n- `resourceRefFromUriSchema` when a boundary accepts a URI string but internal\n  code wants a decoded `HostFileRef`.\n- `absolutePathInputSchema(profile)` only at native/user/host ingress\n  boundaries.\n\nSchemas delegate to the canonical parsers. For example,\n`portableRelativePathSchema` transforms `src/./index.ts` into `src/index.ts`\ninstead of merely branding the original string.\n\n## Resource Keys\n\n`ResourceKey` is an opaque comparison key for maps and deduplication. It may be\ncase-insensitive or Unicode-normalized depending on the selected profile.\n\nDo not persist `ResourceKey` as the only representation of a file. It is lossy by\ndesign and may not preserve display spelling.\n","packages/core/src/primitives/sqlite-store/README.md":"# SQLite stores\n\n`sqlite-store` standardizes how Emdash opens, migrates, tests, backs up, and\nrebuilds SQLite files. It deliberately does not depend on an ORM or a particular\nSQLite package. A host supplies a `SqliteDriver` and may attach an ORM through\n`createOrm`.\n\n## Choose a store type\n\n| Concern | Durable store | Derived store |\n| --- | --- | --- |\n| Intended data | User-authored or otherwise irreplaceable | Cache or index reproducible from another source |\n| Schema changes | Ordered migration history | Drop and rebuild |\n| Version state | `__emdash_migrations` plus runner metadata | Schema fingerprint in `PRAGMA user_version` |\n| Backups | Optional, before pending migrations | Not supported |\n| Downgrade behavior | Unknown newer migration rows are tolerated | Any version mismatch rebuilds |\n| Migration tests | `openAtMigration()` | Rebuild/idempotence tests |\n\nUse `defineDurableSqliteStore()` unless losing the whole database is always safe.\nA derived store's `createSchema` must be capable of recreating everything from\nscratch at any time.\n\n## Defining stores\n\n### Durable\n\nThe application owns the driver adapter and ORM attachment:\n\n```ts\nimport { defineDurableSqliteStore } from '@emdash/core/primitives/sqlite-store/node';\nimport { assertSqliteStoreInvariants } from '@emdash/core/primitives/sqlite-store/testing';\nimport { drizzle } from 'drizzle-orm/better-sqlite3';\nimport { betterSqlite3Driver } from './better-sqlite3-driver';\nimport { migrations } from './migrations.generated';\nimport * as schema from './schema';\n\nexport const appStore = defineDurableSqliteStore({\n  name: 'app',\n  driver: betterSqlite3Driver,\n  migrations,\n  createOrm: (connection) => drizzle(connection.native, { schema }),\n  backup: { retain: 2 },\n  invariants: [assertSqliteStoreInvariants],\n});\n\nconst handle = appStore.open('/path/to/emdash.db');\nhandle.db.select().from(schema.projects);\nhandle.close();\n```\n\n`createOrm` is optional. Without it, `handle.db` is the `SqliteConnection`.\n`postMigrate` hooks run with foreign keys enabled whenever the latest schema is\nopened. `invariants` run after those hooks when migrations were applied, for\nevery `openTemp()`, and after every `migrateToLatest()`. No-op production opens\nskip invariants so full database scans do not become a startup cost.\n\n### Derived\n\n```ts\nimport {\n  defineDerivedSqliteStore,\n  fingerprintDerivedSchema,\n  betterSqlite3Driver,\n} from '@emdash/core/primitives/sqlite-store/node';\n\nconst schemaSql = `\n  CREATE TABLE indexed_paths (\n    id INTEGER PRIMARY KEY,\n    path TEXT NOT NULL\n  ) STRICT\n`;\n\nexport const searchStore = defineDerivedSqliteStore({\n  name: 'file-search',\n  driver: betterSqlite3Driver,\n  version: fingerprintDerivedSchema(schemaSql),\n  createSchema(connection) {\n    connection.exec(schemaSql);\n  },\n});\n```\n\nWhen `user_version` is lower or higher than `version`, the primitive removes all\nnon-internal schema objects, calls `createSchema`, and stamps the requested\nversion in one transaction. Rebuilding a higher version is intentional: derived\nstores remain usable after an application downgrade.\n\n`fingerprintDerivedSchema()` converts the schema SQL itself into a positive\n31-bit `user_version`. It normalizes formatting whitespace outside quoted SQL\nvalues before computing SHA-256, so reformatting or changing line endings does\nnot rebuild production stores. Meaningful DDL and quoted-value changes do. Pass\nthe exact SQL constant executed by `createSchema`; when schema codegen is added,\ncompute the fingerprint from generator input before formatting generated\nTypeScript.\n\n#### Build-time Drizzle codegen\n\n`@emdash/core/primitives/sqlite-store/codegen` exposes\n`compileDrizzleSchemaToSql(schema)`. It uses `drizzle-kit/api` entirely in\nmemory to diff an empty SQLite snapshot against imported Drizzle table exports.\nThe helper sorts export keys before compilation so generated DDL is stable\nacross module loaders.\n\nKeep this import in build scripts and drift tests only. Runtime stores should\nimport a checked-in generated statement array, compute their fingerprint from\nthat array, and never bundle `drizzle-kit`.\n\n## Driver contract\n\n`SqliteConnection` exposes only:\n\n- `exec(sql)` for DDL and multi-statement SQL;\n- `get`, `all`, and `run` for prepared statements;\n- `close`;\n- `native` as an escape hatch for ORM attachment.\n\nThe store—not the driver—owns pragmas and transaction behavior. Every open sets\nWAL mode, `busy_timeout`, and foreign-key enforcement. Transactions use explicit\n`BEGIN IMMEDIATE`, `COMMIT`, and `ROLLBACK`, so different drivers cannot\nsilently change migration semantics. Transaction callbacks must be synchronous.\n\nUse `runSqliteDriverConformance()` from the testing entry point when adding a\ndriver. It verifies multi-statement execution, parameter binding, result shape,\nTEXT/INTEGER/BLOB/NULL round trips, exact large integers, runner pragmas,\nforeign-key toggling, transaction semantics, and `VACUUM INTO`.\n\n## Durable migrations\n\nA bundled migration contains stable identity and raw SQL:\n\n```ts\ntype BundledMigration = {\n  idx: number;\n  tag: string;\n  when: number; // legacy Drizzle journal milliseconds\n  hash: string; // SHA-256 of raw SQL file bytes\n  sql: string;\n};\n```\n\nThe runner:\n\n1. Bootstraps its bookkeeping schema when `user_version` is zero.\n2. Reads applied tags and verifies hashes for migrations known to this binary.\n3. Computes pending work by tag set difference, ordered by `idx`.\n4. Creates a backup when configured and a persistent database needs work.\n5. Disables foreign keys **before** opening migration transactions.\n6. Applies each migration atomically, splitting Drizzle SQL on\n   `--> statement-breakpoint`.\n7. Records `(tag, hash, applied_at)` in `__emdash_migrations`.\n8. Runs optional interop inside the same transaction.\n9. Runs `PRAGMA foreign_key_check`, then reenables enforcement in `finally`.\n\nRows for migrations unknown to an older binary are ignored. This allows additive\ndatabase upgrades to remain downgrade-tolerant. A known tag with a different\nhash throws because shipped migration SQL is immutable.\n\nDurable migration hashes are SHA-256 of the raw SQL file bytes. The algorithm\nis part of the runner schema contract rather than a per-row `hash_algo` column:\nchanging it requires a `RUNNER_SCHEMA_VERSION` bump that explicitly rewrites\nbookkeeping. Derived fingerprints need no stored algorithm because a changed\nimplementation causes one safe rebuild and then becomes self-consistent.\n\nSQLite ignores `PRAGMA foreign_keys` changes inside a transaction. Migration SQL\nmay contain those pragmas, but correctness comes from the outer runner protocol.\n\n### Legacy migration interop\n\nMigration history conversion is optional and pluggable:\n\n```ts\ntype MigrationInterop = {\n  backfill?(connection, migrations): void;\n  onApplied?(connection, migration): void;\n};\n```\n\n`backfill` runs once inside the bookkeeping bootstrap transaction. `onApplied`\nruns inside each migration transaction after the new bookkeeping row is\ninserted.\n\n`drizzleV0Interop` supports databases written by Drizzle's v0 journal layout. It:\n\n- reads `__drizzle_migrations`;\n- matches rows by `created_at === migration.when`, falling back to SQL hash;\n- throws rather than guessing if a row cannot be identified;\n- dual-writes newly applied migrations to `__drizzle_migrations`.\n\nDual-write keeps older application binaries from reapplying migrations after a\ndowngrade. Configure this interop only for stores with that legacy history.\n\n## Backups and restore\n\nDurable stores with `backup: { retain: N }` create consistent sibling backups\nusing `VACUUM INTO` before pending migrations. Paths are SQL-escaped, and old\nbackups are pruned by the creation timestamp embedded in their filename.\n\nCall `restoreLatestBackup(path)` only when every handle for that store path is\nclosed. Restore removes stale WAL/SHM sidecars and replaces the database through\na temporary file. The method refuses to run while the path is open.\nThat open-path guard is scoped to one store instance; callers must share the\ndefined store rather than independently defining stores for the same file.\n\n## Testing\n\n### Fully migrated temporary database\n\n`openTemp` creates an isolated on-disk database, applies all migrations, awaits\nthe optional seed, and deletes the database plus WAL/SHM files on close:\n\n```ts\nconst handle = await appStore.openTemp(async ({ db }) => {\n  await db.insert(projects).values({ id: 'project-1', name: 'Example' });\n});\n\ntry {\n  // test against handle.db or handle.connection\n} finally {\n  handle.close();\n}\n```\n\nIt is intentionally file-backed rather than `:memory:` so WAL, backup, and\nmulti-connection behavior stay representative.\n\n### Testing a specific migration\n\n`openAtMigration(n)` applies migrations whose `idx < n`. Seed pre-migration data\nwith raw SQL, because latest-schema ORM types cannot faithfully describe an old\nschema:\n\n```ts\nconst handle = appStore.openAtMigration(18);\ntry {\n  handle.connection.exec(`\n    INSERT INTO workspaces (id, type, ssh_connection_id)\n    VALUES ('workspace-1', 'project-ssh', 'missing-ssh')\n  `);\n\n  handle.migrateToLatest();\n  assertSqliteStoreInvariants(handle.connection);\n} finally {\n  handle.close();\n}\n```\n\nThis replaces opaque binary pre-migration fixtures with reviewable, adversarial\ntest setup. `openAtMigration` and backup/restore exist only on the durable store\ntype.\n\n`assertIncrementalMigrationEquivalence(store, migrationCount)` opens every\nhistorical migration boundary, migrates it to latest, and compares its\n`sqlite_schema` with a one-shot latest database. Use it as a compact regression\ntest for the complete migration chain.\n\n## Invariants\n\n- Never edit SQL for a shipped migration. Hash verification will reject it.\n- Hash raw UTF-8 file bytes without trimming or line-ending normalization.\n- Preserve legacy `when` milliseconds while `drizzleV0Interop` is needed.\n- `PRAGMA user_version` belongs exclusively to the store primitive: durable\n  stores use it for runner metadata; derived stores use it for a schema\n  fingerprint.\n- Domain code must not read or write `user_version`.\n- Derived data must be safe to delete at any time.\n- Store initialization after migration throws on integrity failures.\n  `assertSqliteIntegrity` runs SQLite's full B-tree and index consistency check;\n  `assertForeignKeyIntegrity` scans for orphaned references. This intentionally\n  differs from expected operational failures that use `Result`: callers cannot\n  safely continue with an unidentified or corrupt schema.\n","packages/core/src/runtimes/lsp/README.md":"# LSP runtime\n\nBuffers use the host LSP runtime for hover, definition, type definition, references\nand diagnostics, according to each server's capabilities. `typescript-language-server`\n6.0.1 with TypeScript 6.0.3 and Pyright 1.1.414 ship with desktop and workspace-server.\nThey start only when needed; opening a Python file does not launch the TypeScript\nserver. Nothing is downloaded\nwhen a file opens. Other servers can be installed or selected in machine dependencies.\nRight-click a file tab and choose **Language services…** for status and restart;\nthere is no persistent language-service indicator in the file header.\nThe executable runs on the host containing the worktree, including SSH hosts.\nRemote language services are included in workspace protocol 12.0.\n\nThe LSP worker starts on the first language-service request in both desktop and workspace-server\nhosts. Its startup is excluded from required host readiness, so a failed or pending LSP startup does\nnot block unrelated runtimes. A later request can retry failed startup; concurrent requests share\none attempt, and successful readiness stays cached. Individual language servers still start only\nwhen a session is attached.\n\n| Languages | Server | Host requirements |\n| --- | --- | --- |\n| TypeScript, JavaScript, JSX, TSX | [typescript-language-server](https://github.com/typescript-language-server/typescript-language-server) with TypeScript 6 | Bundled, runs with Emdash’s Node/Electron; no external Node required |\n| Bash, sh | [bash-language-server](https://github.com/bash-lsp/bash-language-server) | Node.js 20+; ShellCheck on PATH adds lint diagnostics |\n| Go | [gopls](https://go.dev/gopls/) | Go toolchain; make GOBIN or GOPATH/bin available on PATH after installing |\n| Rust | [rust-analyzer](https://rust-analyzer.github.io/book/installation.html) | Rust toolchain, Cargo and rust-src; the offered rustup command installs the analyzer and sources |\n| Python | [Pyright](https://github.com/microsoft/pyright) | Bundled, runs with Emdash’s Node/Electron; uses the project’s Python environment |\n| C, C++ | [clangd](https://clangd.llvm.org/installation) | Project compiler flags, normally compile_commands.json; Homebrew LLVM's bin directory must be on PATH or selected explicitly |\n| JSON, JSONC | [VS Code JSON server](https://github.com/hrsh7th/vscode-langservers-extracted) | Node.js; JSONC includes .jsonc, tsconfig.json and jsconfig.json |\n| YAML | [yaml-language-server](https://github.com/redhat-developer/yaml-language-server) | Node.js; schemas may be associated using a file modeline |\n| HTML, CSS, SCSS, Less | [VS Code HTML/CSS servers](https://github.com/hrsh7th/vscode-langservers-extracted) | Node.js; HTML, CSS and JSON executables share one npm install package |\n\nBash selection includes `.sh`, `.bash` and known startup files such as `.bashrc`;\nthere is no shebang discovery for arbitrary extensionless scripts. Zsh and Fish\ndo not activate the Bash server. Navigation capabilities vary: for example, schema\nlanguages principally offer hover and validation, not programming-language type\ndefinitions. Monaco retains its existing completion and formatting providers;\nhost-backed LSP completion, formatting and workspace edits are not implemented.\n\nHover and navigation select a provider per document and capability. Missing tools,\nstarting/failed servers, unsupported capabilities and disconnected hosts fall back\nto Monaco's bundled services where available (TS/JS, CSS/SCSS/Less, HTML and JSON).\nA successful empty host answer is authoritative. Local TypeScript validation keeps\nthe editor's existing syntax-only policy. There is one diagnostic marker owner per\nmodel: current host publications, including empty ones, supersede local validation;\nedits clear stale diagnostics, and loss of the host restores local validation. Late\nresults cannot replace a newer document or provider's markers. Disk/Git snapshots\nretain local services and are never replicated to a host.\n\nFallback analyzes loaded models and bundled declarations, not the project's filesystem\nor full configuration. It restores basic hover, definition/reference navigation and\nvalidation where Monaco supports them; type-definition navigation still requires host\nLSP. Rust, Go and other languages without Monaco services still need their host server.\nThe language-services dialog explains basic support and offers host-server retry.\n\nThe Monaco adapter owns provider selection and model lifetimes. Local worker access\nand diagnostic arbitration are separate editor modules, with no host or Wire\nknowledge. Automatic registrations for overlapping Monaco features are disabled;\nthe editor invokes the local workers as fallback. TypeScript reuses Monaco's worker;\nCSS, HTML and JSON fallback workers start lazily and are released after two minutes\nof inactivity. Host availability never changes global language defaults.\n\n[editor/browser/lsp/monaco-language-services.ts](../../../../../apps/emdash-desktop/src/core/features/editor/browser/lsp/monaco-language-services.ts) adapts Monaco models, providers,\ndiagnostics and navigation. It registers providers once during lazy Monaco bootstrap.\n[editor/api/browser/lsp/language-service-client.ts](../../../../../apps/emdash-desktop/src/core/features/editor/api/browser/lsp/language-service-client.ts) owns project discovery, session\nsharing, buffer lifetime, cancellation and synchronization before queries. Its document\nbindings expose semantic queries without requiring Monaco or session keys. File tabs\nregister the originating task and workspace root before their buffer models are created. Only `emdash-buffer:` models are\nreplicated; disk and Git snapshots never enter the language server. Shared model\nlifetime owns open/close, so split panes and dirty buffers surviving tab closure\nremain consistent. `DocumentSynchronizer` coalesces edits and flushes every open\nbuffer before a query. Unchanged versions are checked before reading model text.\nAfter the initial snapshot, the synchronizer sends one compact UTF-16 edit against\nthe last acknowledged version. A mismatched base requests a fresh snapshot;\nreconnects also replay snapshots. The host retains complete text for restart and\nexpands edits only when a server requires full-document synchronization. The shared\nfile store emits successful saves with the exact text written to disk. That immutable\nsnapshot crosses Wire with the save notification; the host uses it for didSave's\noptional text instead of its newer live overlay. Synchronization can keep newer edits\nwithout rolling the server document back. The store leaves those edits dirty and\nrecoverable, and isolates synchronous and asynchronous save-observer failures from\nthe completed write. Results preserve host identity and the originating task when\nnavigating outside the root.\n\n`LanguageSessionClient` leases typed live state through the editor Wire domain.\nIts `attachedClient` promise means the Wire attachment exists; server readiness is\na separate live state. Connection state is separate from authoritative server status.\nIndividual request failures are reported to the caller without marking the server\nas failed or clearing diagnostics. Cancelled and stale queries are discarded. The\nNode editor controller routes to `RuntimeBroker`; it does not own processes or\nLSP protocol state. `packages/core/src/runtimes/lsp/` owns process framing,\ninitialization, document versions, cancellation, capabilities, diagnostics and\nshutdown. It runs as a worker on both desktop and workspace-server, with one\nserver per renderer client, resolved project root and server ID. Server commands come\nfrom bundled assets or host dependency descriptors, never renderer-supplied shell text. A live-state\nlease keeps the server alive; the last detach releases it after a short grace\nperiod. Explicit restart replays unsaved buffers, and replacement generations\ninvalidate renderer synchronization caches.\n\nThe server registry uses UTF-16 positions and a two-million-character document limit. Adding\nanother server means adding portable selection metadata in [lsp/api/server-catalog.ts](api/server-catalog.ts)\nand a host profile in [lsp/node/server-registry.ts](node/server-registry.ts), then exercising its capability\nnegotiation and synchronization behavior. The renderer derives provider selectors,\nprotocol language IDs, session keys and labels from that metadata. Monaco-specific\nlanguage mappings remain in the editor adapter; Core contains only protocol language\nIDs. Host profiles own executable descriptors, arguments, project-root markers and configuration;\nthe process transport only binds their supplied request handlers to JSON-RPC.\n\nProject discovery runs through Wire on the file's host before acquiring a session.\nFor TS/JS, the nearest `tsconfig.json`, `jsconfig.json` or `package.json` between the\nfile and task workspace selects the root; absent markers and external definition\ntargets retain the task workspace root. Automatic selection uses the bundled\n`typescript-language-server`. The TypeScript profile resolves the project's\n`tsserver.js`, including hoisted dependencies, and falls back to the bundled TS6\ncompiler if the project has no compatible server. Resolution does not execute\nproject code. Explicit machine-dependency selections override the bundled launch;\ninvalid selections remain visible as errors. Automatic typing acquisition is\ndisabled so opening a JavaScript file never starts npm downloads. Go recognizes\n`go.mod`/`go.work`; Rust recognizes `Cargo.toml` and\n`rust-project.json`; Python recognizes Pyright, pyproject and common Python project\nfiles; C/C++ recognizes compilation databases, `.clangd` and CMake roots. Each uses\nthe nearest marker inside the task workspace. Bash, JSON, YAML, HTML and CSS use\nthe task workspace root without borrowing a nearby TypeScript package root.\n\nThe Python host profile discovers `.venv`, then `venv`, then a captured `VIRTUAL_ENV`\nwith an executable interpreter, without running project code during discovery.\nOtherwise Pyright discovers Python from the host environment. Pyright continues\nto read `pyrightconfig.json` and `pyproject.toml`; diagnostics are limited to open\nfiles. Go and Rust inherit the captured host toolchain environment. The process\ntransport remains independent of these policies.\nHost configuration is resolved again when a server restarts.\n`vscode-languageserver-protocol` (MIT) supplies JSON-RPC framing and cancellation.\nTypeScript 6.0.3 and `typescript-language-server` 6.0.1 (both Apache-2.0) are pinned\nproduction dependencies, alongside Pyright (MIT). Both servers use the host's\nEmdash Node/Electron executable; Electron launches them with `ELECTRON_RUN_AS_NODE`.\nThe same JavaScript assets work on every supported CPU architecture. Electron\nunpacks the servers and their runtime data, with an explicit resource-copy rule\npreserving TypeScript's `.d.ts` standard libraries, which its default module\nfilter would otherwise remove. Workspace-server stages the same pinned versions.\nRelease validation rejects missing assets or accidental resolution outside the\nproduction deployment. The repository's native-preview compiler remains a\ndevelopment build tool and is not a bundled language server.\n\nAfter Electron finishes packaging, native-architecture release builds also launch\nboth servers using the finished app's executable with an empty PATH and isolated\nhome. The check follows definitions into the shipped TypeScript declarations and\nPyright stubs and verifies type diagnostics. Cross-compiled apps need the same check\non a matching host via `scripts/release/verify-language-servers.ts --executable <binary>\n--resources <resources-directory>`. It does not modify the app or launch its UI.\nThe dedicated CI packaging job exercises stable and canary Electron packages and\nverifies that removing standard-library assets makes the check fail.\nThe probe reuses the existing MIT-licensed LSP protocol client as a direct desktop\ndevelopment dependency instead of implementing a second JSON-RPC transport.\n\nBash, YAML and `vscode-langservers-extracted` remain development-only integration\nfixtures. The extracted web-server package brings TypeScript 4.9.5 transitively;\nit is not shipped as our TypeScript server or as a production dependency. Monaco\nsupplies basic HTML/CSS/JSON support without adding that aggregate server package.\n\nThe Wire contract names snapshot replacement (`setDocumentSnapshot`), versioned\nedits (`applyDocumentEdit`), save notification (`documentSaved`) and process recovery\n(`restartServer`) explicitly. Definition, type-definition and reference queries are\nseparate operations; references preserve the caller's `includeDeclaration` option.\nSnapshot replication is required for initialization and recovery, not compatibility.\nThis unreleased feature is included in protocol 12.0, with no legacy LSP aliases.\n\nTests cover protocol state and actual server behavior in Core, host routing and\nbuffer policy in desktop Node tests, and Monaco providers/navigation in browser\ntests. [language-services.e2e.browser.test.ts](../../../../../apps/emdash-desktop/src/core/features/editor/browser/lsp/language-services.e2e.browser.test.ts) carries actual Wire frames through\nPlaywright bindings into the production editor controller and host runtime,\nthen checks cross-file unsaved types, definitions, diagnostics and restart with\nreal TypeScript and Python servers. The bindings exist only in Vitest's test harness.\nCore's [language-servers.integration.test.ts](node/language-servers.integration.test.ts) additionally checks production host\nprofiles against real Bash, JSON/JSONC, YAML, HTML, CSS/SCSS/Less servers in ordinary\ntest runs. Enable its native Go, Rust and C++ cases with installed `gopls`, `go`,\n`rust-analyzer`, `cargo`, `rustc` and `clangd` on PATH:\n\n```bash\nEMDASH_TEST_NATIVE_LSP=1 pnpm --dir packages/core exec vitest run src/runtimes/lsp/node/language-servers.integration.test.ts\n```\n\nThis opt-in fails if a requested tool is absent; it does not install tools or silently\nskip a broken setup. It covers navigation, type definitions where supported, unsaved\nedits, restart replay and lease cleanup. Schema and stylesheet cases also verify\ndiagnostics arriving and clearing after unsaved edits.\n","packages/shared/README.md":"# @emdash/shared\n\n`@emdash/shared` is the repo's standard library: package-level foundations reused\nby Wire, Core, desktop, workspace-server, and tests. These primitives are\nintentionally generic — they do not define product domains, Wire protocol\nmessages, or host-specific process behavior.\n\n## Standard-Library Stance\n\nWorking primitives keep their exports regardless of measured consumers. A\n`Result` type without `map`/`andThen` is half a type, so the full combinator\nAPI stays exported even when a usage census finds few callers; the same goes\nfor the request middleware family, `createDurableQueue`,\n`createSharedResource`, and the logger utilities. Do not re-litigate\nzero-consumer exports in the next census. What this stance does **not** cover\nis genuine machinery: internals such as timer-delay normalization stay\nunexported.\n\n## The Prelude\n\nThe root entrypoint (`@emdash/shared`) is the single home for the blessed\ncross-cutting core:\n\n- The result module: `Result<T, E>`, `ok`/`err`/`fail`, guards, combinators\n  (`map`, `andThen`, `tryCatch`, `gen`, …), and the serialization/error family\n  (`Serializable`, `SerializedError`, `BaseError`, `toSerializedError`,\n  `resultSchema`).\n- `Unsubscribe` and the lifecycle leases: `Lease`, `PendingLease`,\n  `toPendingLease`, `once`.\n- `createEmitter` and the `Emitter<T>` interface.\n- `isDeepEqual`.\n- `Secret`: `secret`, `isSecret`, `reveal`, `REDACTED`.\n\nEvery symbol has exactly one home package-wide: prelude symbols come from the\nroot, domain symbols from their subpath. Nothing is exported from both.\n\n## Entrypoints\n\n- `@emdash/shared` — the prelude (above).\n- `@emdash/shared/concurrency`: `Scope` (`createScope`), `createLifecycleRegistry`,\n  `createConcurrencyLimiter`, `createKeyedLanes`, `createMailbox`,\n  `createAsyncCache`, `createResourceCache`, `createSharedResource`,\n  `createDurableQueue`, `createBoundedBuffer`, and `Disposable`.\n- `@emdash/shared/scheduling`: `Clock`, `systemClock`, `abortableWait`,\n  `abortReason`, `throwIfAborted`, `waitWithSignal`, `retry`, `retrySchedule`\n  and the `retrySchedules` combinators, `runWithTimeout`, `TimeoutError`, and\n  `TimerHandle`.\n- `@emdash/shared/requests`: request orchestration — `compose` middleware,\n  `withRetry`, `withTimeout`, `withScheduler`, `deduplicate`,\n  `createRequestScheduler`, and `createTokenBucketGate`.\n- `@emdash/shared/logger`: the browser-safe logging surface — `log`,\n  `runWithLogger`, levels, formatting, field preparation, and redaction.\n- `@emdash/shared/logger/node`: the Node-only surface — `initProcessLogging`,\n  `createPinoLogger`, `createFileTransport`, and `installAsyncLogContext`.\n- `@emdash/shared/plugins`: the plugin framework — capabilities, assets,\n  registries, and `PluginIconAsset` (the one icon-asset type; never re-alias it).\n- `@emdash/shared/testing`: `deferred`, `createManualClock`,\n  `createStubLogger`, and `waitFor`.\n- `@emdash/shared/util`: stable utility helpers such as `stableStringify`.\n- `@emdash/shared/markdown`: mention grammar helpers.\n- `@emdash/shared/config`: layered config parsing with zod schemas.\n- `@emdash/shared/perf` and `@emdash/shared/perf/node`: spawn accounting and\n  process vitals instrumentation.\n\n## Choosing Lifecycle Primitives\n\nUse `Scope` for ownership, cancellation, cleanup ordering, and async work that\nmust not outlive its feature. Use `scope.add()` and `scope.use()` for finalizers,\n`scope.child()` for nested ownership, and `scope.run()` for tracked async work.\n\nUse `createLifecycleRegistry` when a feature owns a keyed set of local resources\nwith explicit `start()` and `stop()` commands, typed start/stop results,\nqueryable state, and state-change observers. It is a state registry, not a lease\ncache or protocol primitive.\n\nUse `createResourceCache` when resource lifetime is demand-driven: consumers call\n`acquire()`, hold leases, release them, and optionally benefit from an idle TTL.\nUse `createSharedResource` for the same lease behavior around one unkeyed\nresource, and `createAsyncCache` for retryable cached async values with no\nfinalizer.\n\nThe command/event/effect machine primitive is internal to `@emdash/core`'s acp\nruntime (`packages/core/src/runtimes/acp/node/machine/primitive/`) and is not\noffered as a shared building block. Use Wire-owned primitives when the lifecycle is\nprotocol-specific: Wire workers belong to `@emdash/wire/worker` because they\nsupervise process generations and keep a stable typed Wire client, and\n`LiveJobSource` belongs to `@emdash/wire/live` because it publishes cancellable\njob state, progress, retention, and remote client handles.\n\n## Package Conventions\n\n- **Ownership-drop**: primitives that take ownership of values fire `onDrop`\n  exactly once for every taken-then-discarded value, and never reject because\n  of it.\n- **Never-silent**: where a failure would otherwise vanish, optional failure\n  hooks default to logger-backed reporting rather than swallowing (for example\n  the emitter's `onSubscriberError` and keyed-lane coalesce failures fall back\n  to `log.warn`).\n- **Clock seam**: no raw `Date.now`/`setTimeout` outside `scheduling/clock.ts`;\n  time-dependent primitives accept a `Clock`.\n","packages/wire/docs/README.md":"# @emdash/wire Docs\n\n`@emdash/wire` is the transport-agnostic runtime layer for typed API calls,\nlive model subscriptions, live logs, event streams, jobs, mutations, workers, and\nthe small utilities that sit at the API boundary.\n\nWire builds on Shared foundations for generic lifecycle, scheduling,\nconcurrency, testing, and stable utility behavior:\n\n```mermaid\nflowchart TB\n  subgraph shared [Shared foundations used by Wire]\n    scope[Scope and ResourceCache]\n    registry[LifecycleRegistry]\n    scheduling[Clock and RetrySchedule]\n    mailbox[Mailbox]\n  end\n  subgraph api [API layer]\n    contracts[defineContract endpoint kinds]\n    controller[createController]\n    client[contract client]\n    transports[Transports: memory, port, dom-port, electron, stream, reconnecting]\n  end\n  subgraph live [Live primitives]\n    model[LiveStateSource and replicas]\n    log[LiveLogSource]\n    eventStream[EventStream]\n    job[LiveJobSource]\n    mutations[Mutations and registries]\n  end\n  subgraph workers [Wire workers]\n    component[WireComponent]\n    workerHost[WireWorkerHost]\n  end\n  shared --> api --> live\n  component --> api\n  workerHost --> api\n  workerHost --> component\n```\n\nThe live layer owns the stateful primitives: `LiveStateSource`, `LiveLogSource`,\n`EventStreamSource`, `LiveJobSource`, and consumer-instantiated replicas; live\nmodels are served by providers, usually `expose()` over kernel state. Low-level `*Client` followers track cursors and resync, while\nmaterializers (`StateStore`, `LogSink`, `JobStore`) own values. Most consumers use\nclient handles directly or wrap them in replicas. The API layer turns those\nprimitives into a contract with typed procedure calls and live topic client\nhandles. `WireComponent` is the reusable contract implementation pattern: it\ndeclares explicit typed requirements, validates config at the creation boundary,\nand can be created in-process or hosted in a worker. Worker hosting is Wire-specific because it\nserves components across processes. Generic lifecycle, scheduling, concurrency, testing, and stable\nutility primitives live in `@emdash/shared` and are documented here where Wire\nuses them. The `WireInstrumentation` seam is cross-cutting and can be attached\nto API, live, and worker surfaces through their options.\n\n## Pages\n\n- API:\n  - [Contracts](./api/contracts.md): `defineContract()`, endpoint kinds, nested\n    composition, and live model groups.\n  - [Serving and clients](./api/serving.md): `createController()`, `serve()`,\n    `connect()`, cancellation, controller composition, session hubs, and\n    server-side call helpers.\n  - [Composable middleware](./api/middleware.md): target-first `compose()`,\n    handler middleware, controller middleware, timeout, retry, and\n    deduplication.\n  - [Typed clients](./api/clients.md): `ContractClient` handles,\n    `forwardController()`, and selective forwarding through `createController()`.\n  - [File endpoints](./api/files.md): `downloadFile()`, `uploadFile()`, blob\n    channels, and binary stream transport framing.\n  - [Wire errors](./api/errors.md): error planes, `WireErrorCode` meanings,\n    origins, and retry guidance.\n  - [Transports](./api/transports.md): memory, ports, Electron, streams, and\n    reconnecting transports.\n- Live:\n  - [Live models and protocol](./live/live-model.md): snapshots, updates,\n    cursors, `LiveStateSource`, and replicas.\n  - [Live logs](./live/live-log.md): retained terminal-style logs and client\n    callbacks.\n  - [Event streams](./live/event-stream.md): keyed fire-and-forget events with\n    explicit gap callbacks after reattach.\n  - [Live jobs](./live/live-job.md): progress, cancellation, terminal state,\n    retention, and contract job handles.\n  - [Mutations](./live/mutations.md): mutation ids, host contexts, cursor settling,\n    idempotency cache, and retry behavior.\n  - [Replicas](./live/replicas.md): `LiveModelReplicaCache`, `LiveLogReplicaCache`,\n    `LiveJobReplicaCache`, pluggable stores, ref counting, and serving cached state.\n- Runtime:\n  - [Lifecycle utilities](./runtime/lifecycle.md): Shared `Scope`,\n    `LifecycleRegistry`, scope loggers, `describeScope()`, and resource\n    ownership.\n  - [Structured concurrency](./runtime/structured-concurrency.md): `Scope.run()`,\n    run cancellation, lifecycle invariants, and diagnostics.\n  - [Scheduling](./runtime/scheduling.md): Shared `Clock`, `TimerHandle`,\n    `ManualClock`, retry schedules, abortable sleeps, and timer ownership.\n  - [Resource caches](./runtime/resource-cache.md): Shared `ResourceCache`,\n    `SharedResource`, `AsyncCache`, identity rules, and lease behavior.\n  - [Mailbox and Broadcast](./runtime/mailbox-and-broadcast.md): Shared bounded\n    local async handoff, overflow policy, guarantees, and the deferred Broadcast\n    contract.\n  - [Components](./runtime/components.md): `defineWireComponent()`, explicit\n    requirements, in-process creation, worker deployment, and non-DI composition\n    rules.\n  - [Workers](./runtime/workers.md): `WireWorkerHost`, one-generation spawners,\n    `runWireComponentWorker()`, and process-hosted components.\n- [Observability](./observability.md): ambient logger context, the\n  `WireInstrumentation` seam, and scope loggers.\n\nRunnable examples live under [../examples](../examples). Most snippets in these\ndocs are shortened versions of those files.\n\n## Package Exports\n\nThere is no root export: every symbol has exactly one home in a hand-curated\nsubpath entrypoint.\n\n```ts\nimport { createController, defineContract } from '@emdash/wire/rpc';\nimport { cell, expose } from '@emdash/wire/state';\n```\n\n- `@emdash/wire/rpc`: contract definition (`defineContract()`, endpoint\n  factories), controller creation, clients, `connect()`/`serve()`, transports,\n  protocol vocabulary (`WireMessage`, `WireTransport`, `WireError`,\n  `LiveSource`, `LiveUpdate`, cursors), provider seam types\n  (`LiveModelProvider`, `LeasedLiveModelProvider`,\n  `LiveModelMutationEnvelope`), the blob/file surface, validation, and the\n  `WireInstrumentation` seam types. Retry schedules come from\n  `@emdash/shared/scheduling`.\n- `@emdash/wire/live`: the server-side reactivity sources (`LiveLogSource`,\n  `LiveJobSource`, `EventStreamSource`, `createEventStreamHost()`), the keyed\n  replica caches (`createLiveLogReplicaCache()`, `createLiveJobReplicaCache()`,\n  `ReplicaLog`), and resync failure policies (`resyncRetry()`,\n  `resyncMarkStale()`).\n- `@emdash/wire/state`: state kernel primitives and Wire bridges (`cell`,\n  `query`, `family`, `optimistic`, `observe`, `expose`, `remote`).\n- `@emdash/wire/mobx`: MobX-backed log/store helpers\n  (`createImmutableMobxStore`, `createReactiveMobxStore`, `createMobxLogStore`).\n- `@emdash/wire/testing`: Wire test helpers such as `createTestWire()` and fake\n  worker process support.\n- `@emdash/wire/worker`: `WireWorkerHost`, `runWireComponentWorker()`,\n  `defineWireComponent()` and component requirement helpers, worker signal\n  types, supervision types, process contracts, and worker log forwarding.\n- `@emdash/wire/worker/node`: Node `childProcessSpawner()`.\n\nUse Shared subpaths directly for generic foundations:\n\n- `@emdash/shared/concurrency`: `Scope`, `Run`, `LifecycleRegistry`, `Mailbox`,\n  `ResourceCache`, `SharedResource`, `AsyncCache`, bounded buffers, and disposable\n  helpers.\n- `@emdash/shared/scheduling`: `Clock`, `systemClock`, `TimerHandle`,\n  `TimeoutError`, `runWithTimeout()`, `RetrySchedule`, retry schedule builders,\n  and `retry()`.\n- `@emdash/shared/requests`: request handler composition, timeout, retry,\n  in-flight deduplication, and request scheduling.\n- `@emdash/shared/testing`: `createManualClock()`, `deferred()`, `waitFor()`, and\n  stub logger helpers.\n- `@emdash/shared/util`: stable utility helpers such as `stableStringify()`.\n\nMobX-backed utilities intentionally live in their own export because they have a\n`mobx` peer dependency. Server-only code can import `@emdash/wire/rpc`,\n`@emdash/wire/live`, `@emdash/wire/state`, `@emdash/wire/worker`, and Shared\nfoundation subpaths without pulling in MobX.\n\n## Typical Flow\n\n1. Define a contract with `defineContract({ ... })`.\n2. Create server-side `cell`, `query`, `LiveLogSource`, `EventStreamSource`, or `LiveJobSource`\n   instances and publish live models with `expose()`.\n3. Use `family()` or explicit domain indexes for keyed live model resources.\n4. Create a controller with `createController(contract, impl)`. Validation is\n   applied by default (`'full'` outside production, `'inputs'` in production);\n   pass `{ validate }` to override.\n5. Serve the controller over a `WireTransport`.\n6. Connect from the client and create a typed `client()`.\n7. Use client handles directly for streaming, or use `remote()` for local state,\n   ref counting, and downstream observation.\n\nFor a complete state kernel example (`cell` + `expose` on the server,\n`remote()` on the client), see [../examples/state-kernel](../examples/state-kernel).\n\n## Event Stream Attachment Law\n\nEvent streams retain no events, and a connection deduplicates attachments to the same topic.\nAnything a late attacher must learn therefore has to live in a retained snapshot or in the attach\nacknowledgment, never in an event-stream message. A one-shot `ready` event is not a valid readiness\nbarrier: a consumer joining an already-attached topic will not cause the server to emit it again.\n\nUse `resourcedStream()` when a successful attach must guarantee that an underlying resource is\nready. Its host must be created with `createEventStreamHost(def, { activate })`, where `activate`\nreceives an `AbortSignal` and resolves to the activation-owned disposer only after the resource is\nready. The source registers the subscriber before activation begins, shares and retains the\nactivation promise for all subscribers to the key, and invokes the disposer when the final\nsubscriber leaves. If the final subscriber leaves while activation is pending, the signal aborts\nand any late result is disposed. Failed activation is not retained, so a later attach makes a fresh\nattempt.\n\nThis guarantee is enforced when a controller binds the endpoint: a resourced definition accepts\nits matching host or a forwarded client handle, but rejects a bare resolver. Forwarding preserves\nthe barrier because the downstream attach is acknowledged only after the upstream attach is\nacknowledged.\n","packages/wire/docs/api/clients.md":"# Typed Clients\n\n`client(contract, connection)` creates a typed `ContractClient`. It has the\nsame nested shape as the contract, but it does not materialize live state.\n\nThink of it as a type-safe wrapper around `Connection`: it knows contract paths,\ninput/output types, live topic ids, group keys, and mutation envelopes, but it does\nnot keep snapshots, apply patches, or settle mutations against local state.\n\n```ts\nconst contractClient = client(api, connect(transport));\n\nawait contractClient.ping({ message: 'hello' });\n\nconst state = contractClient.conversation.state(key, 'state');\nconst snapshot = await state.snapshot();\nconst detach = await state.attach((update) => {\n  console.log(update.delta);\n});\n```\n\n## Endpoint Shapes\n\nEach contract endpoint becomes a protocol client handle:\n\n- Procedures become typed functions: `contractClient.ping(input, { signal? })`.\n- Live logs become `LiveLogClientHandle`s: `contractClient.output.handle(key)`.\n- Event streams become `EventStreamClientHandle`s:\n  `await contractClient.events.subscribe(key, { onEvent, onGap?, onError? })`.\n- Live models become `LiveModelClientHandle`s:\n  `contractClient.conversation.state(key, 'state')` and\n  `contractClient.conversation.mutate('setTitle', envelope)`.\n- Jobs become `LiveJobClientHandle`s: `contractClient.build.start(input)`,\n  `contractClient.build.cancel(jobId)`, and `contractClient.build.handle(jobId)`.\n\n`LiveClientHandle` is the common live handle:\n\n```ts\ntype LiveClientHandle<T> = {\n  topic: string;\n  snapshot(): Promise<LiveSnapshot<T>>;\n  attach(\n    push: (update: LiveUpdate) => void,\n    options?: {\n      onReattach?: () => void;\n      onReattachError?: (error: WireError, context: { retrying: boolean }) => void;\n    }\n  ): Promise<Unsubscribe>;\n  asLiveSource(): LiveSource;\n};\n```\n\n`snapshot()` and `attach()` are useful for low-level code. For example, PTY output\ncan stream directly into an xterm instance without allocating another store:\n\n```ts\nconst output = contractClient.ptyOutput.handle({ sessionId });\nterm.write((await output.snapshot()).data.text);\n\nconst detach = await output.attach((update) => {\n  term.write((update.delta as { chunk: string }).chunk);\n}, {\n  onReattach: async () => {\n    term.reset();\n    term.write((await output.snapshot()).data.text);\n  },\n  onReattachError: (error, { retrying }) => {\n    if (!retrying) showOutputError(error.message);\n  },\n});\n```\n\nWhen a process wants local state, pass the client handle to a replica wrapper instead.\n\n## Forwarding\n\nContract clients are intentionally forwardable. Use `forwardController(contract, client)`\nwhen a hop should serve the same contract without owning state or intercepting\nindividual endpoints:\n\n```ts\nconst upstream = client(api, connect(sshTransport));\nconst controller = forwardController(api, upstream);\n```\n\nNo live state is created at that hop. The downstream client sees the same contract,\nwhile calls, snapshots, live attachments, jobs, and group mutations are forwarded to\nthe upstream connection. Event stream handles are forwarded as live attachments with\nthe same at-most-once delivery semantics as the upstream connection.\n\nForwarding also preserves upstream live lifecycle. A downstream subscriber receives\n`onGap` after the upstream attachment has reattached, and receives `onError` when\nan upstream reattach fails, even if the downstream transport never disconnected.\n\nThis is the right shape for protocol relays and middle tiers that should not own or\ncache state. The hop does not create `ReplicaState`s, does not allocate local\ncursor spaces, and does not run mutation settling. It just preserves the contract\nsurface while delegating to the upstream connection.\n\n`forwardController()` is the preferred shape for full protocol relays because it\nderives the controller implementation from the contract. Adding a new endpoint to\nthe contract does not require a hand-written forwarding entry at each relay. File\ndownloads are adapted internally from the client download handle back into the\ncontroller's streamed download shape.\n\nUse `createController()` directly when forwarding should be selective. A procedure\nmethod can be passed as a procedure implementation, and live client handles can be\npassed as live model, live log, event stream, or live job implementations:\n\n```ts\nconst upstream = client(workspaceApi, connect(sshTransport));\n\nconst controller = createController(workspaceApi, {\n  // Local interception.\n  ping: async () => 'desktop-main',\n\n  // Forwarded subtree.\n  git: upstream.git,\n\n  // Cached group; see Replicas.\n  conversation: createLiveModelReplicaCache(\n    workspaceApi.conversation,\n    upstream.conversation,\n    { lingerMs: 10 * 60_000 }\n  ),\n});\n```\n\nUse this composition style for middle tiers that need local handlers, auditing,\nauthorization checks, replicas, or other endpoint-specific behavior. The explicit\ncontroller implementation makes the interception points visible.\n\n## When to Use Replicas\n\nUse replicas when a process wants local state. See [Replicas](../live/replicas.md).\n\nCommon examples:\n\n- A renderer window wants observable UI state.\n- Electron main wants state that survives renderer reloads.\n- A process wants a local cache before serving the same live model contract to\n  downstream clients.\n\nFor Electron main and other middle tiers that both serve and inspect live state, use\nthe replica local access pattern in\n[Replicas](../live/replicas.md#model-replicas).\n\nDo not materialize just to forward. Forward the client handle or subtree directly.\n","packages/wire/docs/api/contracts.md":"# API Contracts\n\nContracts describe the API surface once. Controller creation and client creation\nderive their types from the same object.\n\n## Defining a Contract\n\nUse `defineContract({ ... })`. Object keys are significant: they determine\nprocedure paths and live ref ids after nested contracts are mounted.\n\n```ts\nexport const notesApi = defineContract({\n  activity: liveLog({ key: sessionKeySchema }),\n  session: liveModel({\n    key: sessionKeySchema,\n    states: {\n      notes: liveState({ data: notesStateSchema }),\n    },\n    mutations: {\n      addNote: mutation(\n        { input: z.object({ text: z.string() }), data: noteSchema, error: z.string() },\n        (ctx, input) => {\n          const note = { id: input.text.toLowerCase(), text: input.text };\n          ctx.produce('notes', (draft) => {\n            draft.notes.push(note);\n          });\n          return ok(note);\n        }\n      ),\n    },\n  }),\n  clearNotes: procedure({\n    input: sessionKeySchema,\n    output: notesStateSchema,\n  }),\n});\n```\n\nSee [../../examples/api-definition/contract.ts](../../examples/api-definition/contract.ts).\n\n## Endpoint Kinds\n\n### `procedure`\n\n`procedure({ input, output })` defines a request/response call. Procedure clients\naccept an optional `{ signal }` call option for cooperative cancellation; see\n[serving](./serving.md#cancellation).\n\n### `fallible`\n\n`fallible({ input, data, error })` defines a procedure whose output is a\n`Result<data, error>` payload:\n\n```ts\nloadNote: fallible({\n  input: z.object({ id: z.string() }),\n  data: noteSchema,\n  error: z.object({ type: z.literal('note_not_found') }),\n});\n```\n\nUse `fallible()` for expected domain failures that callers should handle as data.\nThrown `WireError`s are reserved for infrastructure failures and bugs such as\ndisconnects, unknown paths, and uncaught handler exceptions. See\n[wire errors](./errors.md).\n\n### `mutation`\n\n`mutation({ input, data, error }, handler?)` defines a live-state mutation shape.\nMutations are members of `liveModel()` definitions. Optimistic previews should use\nshared reducer functions with the `optimistic()` bridge rather than contract-local\nclient models.\n\n`mutation()` is for operations that must settle live model cursors. Use\n`procedure()` for calls that do not update live models and `liveJob()` for\nlong-running work.\n\n### `liveLog`\n\n`liveLog({ key })` declares a keyed text log endpoint. It is served by a\n`LiveLogSource` resolver and consumed through a `LiveLogClientHandle` or a\n`LiveLogReplicaCache`.\n\n### `eventStream`\n\n`eventStream({ key, event })` declares a keyed, fire-and-forget event channel.\nEvents are delivered only while a client is attached; the server keeps no\nretained snapshot and late subscribers do not receive historical events.\n\n```ts\nfileEvents: eventStream({\n  key: z.object({ rootPath: z.string() }),\n  event: z.object({\n    kind: z.enum(['create', 'update', 'delete']),\n    path: z.string(),\n  }),\n});\n```\n\nServe event streams with `createEventStreamHost(contract.fileEvents)` or a\nresolver that returns an `EventStreamSource`. Client handles expose\n`await subscribe(key, { onEvent, onGap?, onError? })`. `onGap` runs after\ntransport reattach so consumers can resync state that may have missed events.\nUse `eventStream` for loss-tolerant notifications and invalidation signals; use\n`liveLog` when callers need retained text output and `liveModel` when callers\nneed convergent state. See [event streams](../live/event-stream.md).\n\n### `liveJob`\n\n`liveJob({ input, progress, result, error })` models long-running work with progress,\ncancellation, terminal state, and reattach:\n\n```ts\nbuild: liveJob({\n  input: z.object({ target: z.string() }),\n  progress: z.object({ step: z.string() }),\n  result: z.object({ artifact: z.string() }),\n  error: z.object({ message: z.string() }),\n});\n```\n\n`createController()` binds the endpoint to `{ run, toError? }`. `run` returns a\n`Result<result, error>` payload; `toError` is only an optional fallback for\nunexpected thrown errors. The typed client gets `start(input)`, `cancel(jobId)`,\nand `handle(jobId)` helpers. See\n[live jobs](../live/live-job.md).\n\n### `downloadFile` and `uploadFile`\n\n`downloadFile({ input, meta?, error })` and\n`uploadFile({ input, accept?, maxSize?, result, error })` model byte transfer as a\ntyped call envelope plus a credit-based blob channel. Bytes are streamed in\nbounded chunks and are not embedded in procedure inputs or results:\n\n```ts\nreadAttachment: downloadFile({\n  input: z.object({ id: z.string() }),\n  error: z.object({ type: z.string() }),\n});\n\nuploadAttachment: uploadFile({\n  input: z.object({ conversationId: z.string() }),\n  accept: ['image/png', 'image/jpeg'],\n  maxSize: 25 * 1024 * 1024,\n  result: attachmentRefSchema,\n  error: z.object({ type: z.string() }),\n});\n```\n\nDownload clients receive a single-use handle with `chunks()`, `bytes()`, `file()`,\nand `cancel()`. Upload clients pass a file-like value as the second argument. See\n[file endpoints](./files.md).\n\n### `liveModel`\n\n`liveModel({ key, states, mutations })` aggregates related live states and the\nmutations that may touch them. A single key addresses every member. States are\ndeclared with `liveState({ data })`; the helper creates keyed member refs.\n\n```ts\nconst api = defineContract({\n  conversation: liveModel({\n    key: conversationKeySchema,\n    states: {\n      state: liveState({ data: stateSchema }),\n      usage: liveState({ data: usageSchema }),\n    },\n    mutations: {\n      setTitle: mutation(\n        { input: z.object({ title: z.string() }), data: z.void(), error: z.string() },\n        (ctx, input) => {\n          ctx.produce('state', (draft) => {\n            (draft as { title: string }).title = input.title;\n          });\n          ctx.produce('usage', (draft) => {\n            (draft as { tokens: number }).tokens += input.title.length;\n          });\n          return ok(undefined);\n        }\n      ),\n    },\n  }),\n});\n```\n\nInline group mutation handlers should be pure functions of the member drafts and\ninput: avoid I/O, time, randomness, and server-only state in the inline handler\nbody. If a mutation is schema-only (`mutation(def)`), the server supplies the\nhandler when it creates the live model host.\n\n## Nested Composition\n\nContracts can contain other contracts:\n\n```ts\nconst ptyAgent = defineContract({\n  sessions: liveModel({\n    key: sessionKeySchema,\n    states: { state: liveState({ data: sessionStateSchema }) },\n  }),\n  output: liveLog({ key: sessionKeySchema }),\n});\n\nconst api = defineContract({ ptyAgent });\n```\n\nThe final mount path determines ids and procedure paths:\n\n- `api.ptyAgent.sessions.states.state.id` is `ptyAgent.sessions.state`.\n- `api.ptyAgent.output.id` is `ptyAgent.output`.\n- a procedure at `api.ptyAgent.startSession` is called as `ptyAgent.startSession`.\n- a group member at `api.tasks.conversation.states.state` gets the id\n  `tasks.conversation.state`.\n\nThis lets packages define small contracts locally and compose them into a larger\nworkspace API without an extra namespace argument.\n\n## Live Model Providers\n\nOn the server, implement a live model endpoint with a `LiveModelProvider`. The\nusual way to build one is `expose()` from `@emdash/wire/state`, which resolves\neach contract state to a kernel readable per key:\n\n```ts\nconst conversations = expose(\n  api.conversation,\n  {\n    state: (key) => conversationState(key),\n    usage: (key) => conversationUsage(key),\n  },\n  { mutations }\n);\n```\n\nThen bind the endpoint by passing the provider in the implementation object:\n\n```ts\nconst controller = createController(api, { conversation: conversations });\n```\n\nThe live model client handle exposes `state(key, name)` for member handles and\n`mutate(name, envelope)` for mutation calls. Wrap it in `createLiveModelReplicaCache()`\nwhen a process wants local state, ref counting, and mutation settling. See\n[serving](./serving.md#typed-clients).\n","packages/wire/docs/api/errors.md":"# Wire Errors\n\nWire has two error planes:\n\n- Expected domain failures travel as `Result` payloads. Prefer `fallible()` for\n  procedure contracts whose callers should handle failures as ordinary data.\n- Thrown `WireError`s are reserved for infrastructure failures, contract drift,\n  lifecycle mistakes, and uncaught handler exceptions.\n\n`WireError` instances carry a typed `code`, a message, and an optional serialized\ncause. When an uncaught JavaScript error crosses the wire, the transport serializes\nit as `HANDLER_ERROR` with a clone-safe cause.\n\n## Codes\n\n### `CANCELLED`\n\nThe call was aborted. The client raises it when the caller's `AbortSignal` is\nalready aborted or aborts while a call is pending. The server raises it when it\nreceives a `cancel` message and the handler observes the abort.\n\nRetrying is not automatic and usually not appropriate because cancellation was\ncaller intent.\n\n### `DISCONNECTED`\n\nThe transport disconnected while a request was in flight, posting a request\nfailed because the peer was already gone, the held-call buffer overflowed while\ndisconnected, or the transport failed permanently (the terminal cause is\nattached as `cause`).\n\nMutations are never retried automatically. Callers can opt in per call with\n`retry: { schedule }`; that retry is safe because mutations carry a stable\n`mutationId` and the server deduplicates repeated ids.\n\n### `UNKNOWN_PROCEDURE`\n\nNo procedure handler exists for the call path. Common causes are an incorrect\nclient path, a missing implementation branch in `createController()`, or a drift\nbetween the contract and the controller implementation.\n\nThis is not retryable without changing code or wiring.\n\n### `UNKNOWN_TOPIC`\n\nThe live topic's ref id is not registered by the controller.\n\nThis usually means the client encoded the wrong live ref id, or a controller was\ncreated without the endpoint that owns the ref. It is not retryable without\nchanging the topic or server wiring.\n\n### `NOT_FOUND`\n\nThe live ref id is known, but no live instance exists for the provided key.\nExamples include a live model group instance that has not been created yet or a\njob state that has already been evicted.\n\nRetrying may make sense only if the domain resource is expected to appear later.\n\n### `MISSING_HANDLER`\n\nController creation-time error: a contract endpoint has no implementation. Examples include a\nmissing procedure implementation, missing live resolver, missing job handler, or\nmissing live model provider.\n\nThis should not cross the wire during normal operation. Fix the controller implementation.\n\n### `CONTRACT_MISMATCH`\n\nController creation-time error: a live model provider, replica cache, or client\nhandle was created for a different contract than the endpoint being bound.\n\nThis is not retryable; fix the provider/contract pairing.\n\n### `ALREADY_EXISTS`\n\nA keyed live resource was created for a key that already has a live instance.\n\nThis is a lifecycle error. Dispose or reuse the existing instance instead of\ncreating another one for the same key.\n\n### `TIMEOUT`\n\nA Wire call or snapshot exceeded an infrastructure deadline. The connection\napplies a default 30s call deadline (`callTimeoutMs`, per-call `timeoutMs`\noverride) spanning call-issued to result-received, including time spent held\nwhile disconnected; live attach traffic and blob streaming are exempt. Timeout\nis distinct from `CANCELLED`: cancellation is caller intent, while timeout means\npolicy stopped waiting for the operation.\n\nTimeouts are not retried automatically. Mutation calls that opt in to\n`retry: { schedule }` retry both `DISCONNECTED` and `TIMEOUT`, relying on\nstable `mutationId` deduplication. If a domain treats a timeout as an expected\noutcome, model it as a `Result` payload instead of relying on the\ninfrastructure error plane.\n\n### `HANDLER_ERROR`\n\nAn uncaught exception escaped a server handler or validation boundary. Expected\ndomain failures should be modeled as `Result` payloads with `fallible()` instead\nof thrown.\n\nThe serialized wire error includes the thrown value as a clone-safe cause. Treat\nthis as a bug signal and fix the handler or validation logic.\n\n## Live Attachment Errors\n\nLive attachments use the same infrastructure error plane as calls and snapshots.\nThey do not wrap startup or lifecycle failures in endpoint `Result` payloads.\n\nInitial attachment failures reject the `attach()` or event-stream `subscribe()`\npromise with a `WireError`. Common codes are `DISCONNECTED`, `UNKNOWN_TOPIC`,\n`NOT_FOUND`, and `HANDLER_ERROR`.\n\nOnce an attachment is established, the client keeps it as durable intent across\ntransport reconnects. Reattach failures are delivered to the subscription's\n`onReattachError` or `onError` callback with `{ retrying }`:\n\n- `DISCONNECTED` is reported with `retrying: true`; the attachment stays\n  registered and will try again on the next reconnect.\n- Other `WireError`s are reported with `retrying: false`; the attachment is\n  terminated because retrying the same topic would repeat the same failure.\n\nForwarding preserves these lifecycle notifications. A downstream connection can\nremain healthy while an upstream live attachment reports a gap or terminal error.\nForwarding sends those as live lifecycle messages rather than hiding them behind\nthe downstream transport state.\n\nThese errors are distinct from expected domain events carried by a stream. For\nexample, fs-watch may emit `{ kind: 'error' }` as part of its event payload when a\nnative watcher fails to start. That payload belongs to the fs-watch contract. A\nlive attachment `WireError` means the wire topic itself could not attach or\nreattach.\n","packages/wire/docs/api/files.md":"# File Endpoints\n\n`downloadFile()` and `uploadFile()` stream bytes through a dedicated blob channel\ninstead of embedding `Uint8Array` payloads in procedure inputs or results. The call\npayload carries only typed input, metadata, and a channel id; byte chunks flow after\nthat over credit-based frames.\n\n## Contracts\n\n```ts\nconst api = defineContract({\n  readAttachment: downloadFile({\n    input: z.object({ id: z.string() }),\n    error: z.object({ type: z.string() }),\n  }),\n  uploadAttachment: uploadFile({\n    input: z.object({ conversationId: z.string() }),\n    accept: ['image/png', 'image/jpeg'],\n    maxSize: 25 * 1024 * 1024,\n    result: attachmentRefSchema,\n    error: z.object({ type: z.string() }),\n  }),\n});\n```\n\nFile metadata uses `{ name, mimeType, size?, lastModified? }`. Download endpoints\nmay extend the metadata schema with endpoint-specific fields.\n\n## Server Implementations\n\n```ts\nconst controller = createController(api, {\n  readAttachment: async ({ id }) => {\n    const stored = await store.get(id);\n    if (!stored) return err({ type: 'missing' });\n    return ok({\n      meta: {\n        name: stored.ref.name,\n        mimeType: stored.ref.mimeType,\n        size: stored.data.byteLength,\n      },\n      source: stored.stream(),\n    });\n  },\n  uploadAttachment: async (input, file) => {\n    const bytes = await file.bytes();\n    return ok(await store.put({ data: bytes, mimeType: file.mimeType, name: file.name }));\n  },\n});\n```\n\nThe source can be an `AsyncIterable<Uint8Array>` or a readable stream-like value.\n`WireFile` on upload exposes `stream()`, `bytes()`, `file()`, and `cancel()`.\n\n## Client Usage\n\n```ts\nconst downloaded = await client.readAttachment({ id });\nif (downloaded.success) {\n  for await (const chunk of downloaded.data.chunks()) {\n    // incremental processing\n  }\n}\n\nconst uploaded = await client.uploadAttachment({ conversationId }, file);\n```\n\nDownload handles are single-consumption: choose `chunks()`, `bytes()`, or `file()`.\n`bytes()` verifies `meta.size` when present.\n\n## Protocol Invariants\n\n- Bytes are sent as `blob-chunk` frames and never inside `call`/`result` values.\n- Producers send only after the consumer grants credit with `blob-pull`.\n- Consumers keep a bounded local chunk mailbox aligned with the maximum granted\n  credit. Overflow is treated as a protocol failure; blob chunks are never\n  dropped.\n- `blob-end` is the only successful EOF signal; disconnect before it is an error.\n- `blob-close` cancels a channel and disposes the producer source.\n- Frames for unknown channels are ignored so close/chunk races are harmless.\n- Chunks are re-sliced to standalone buffers before posting to avoid cloning a much\n  larger backing `ArrayBuffer`.\n\nStructured-clone transports carry `Uint8Array` chunks natively. `streamTransport`\nuses length-prefixed binary framing so workspace-server stdio/socket/SSH paths send\nraw bytes with no base64 expansion.\n","packages/wire/docs/api/middleware.md":"# Composable Middleware\n\nWire uses explicit composition for execution policy. Contracts describe protocol\nshape; middleware wraps handlers or controllers at the composition site.\n\n```ts\nimport { compose, deduplicate, withRetry, withTimeout } from '@emdash/shared/requests';\nimport { retrySchedules } from '@emdash/shared/scheduling';\nimport { createController } from '@emdash/wire/rpc';\n\nconst loadStats = compose(\n  async (input: { repo: string }, meta: { signal?: AbortSignal }) => {\n    return await fetchStats(input.repo, { signal: meta.signal });\n  },\n  [\n    withTimeout({ timeoutMs: 20_000 }),\n    withRetry({\n      schedule: retrySchedules.exponential({\n        initialMs: 250,\n        maxMs: 2_000,\n        maxRetries: 3,\n      }),\n      shouldRetry: isTransient,\n    }),\n    withTimeout({ timeoutMs: 5_000 }),\n  ]\n);\n\nconst controller = createController(api, { loadStats });\n```\n\n## `compose()`\n\n`compose(target, middlewares)` accepts the value being wrapped first and a\nreadonly middleware array second:\n\n```ts\ntype Middleware<T> = (next: T) => T;\n```\n\nThe first array entry is outermost. It sees the request first and settles last:\n\n```mermaid\nflowchart LR\n  composeCall[\"compose(handler, middlewares)\"] --> totalTimeout[TotalTimeout]\n  totalTimeout --> retry[Retry]\n  retry --> attemptTimeout[AttemptTimeout]\n  attemptTimeout --> handler[RawHandler]\n```\n\nThis order makes policy boundaries explicit. In the first example, the outer\ntimeout bounds the complete retry program, while the inner timeout bounds each\nindividual attempt.\n\n## Handler Middleware\n\nHandler middleware wraps procedure implementations before `createController()`:\n\n```ts\nconst handler = compose(rawHandler, [\n  withTimeout({ timeoutMs: 10_000 }),\n  deduplicate({ key: (input) => input.repo }),\n]);\n```\n\nUse handler middleware for endpoint-specific execution policy:\n\n- Timeout and deadline policy.\n- Retry policy for idempotent or externally safe operations.\n- In-flight request deduplication.\n- Per-endpoint rate limiting or concurrency limits when those utilities exist.\n\nProcedure handlers receive `(input, meta)`. Middleware must preserve every field\non `meta` and replace only `meta.signal` when it needs derived cancellation:\n\n```ts\nconst wrapped = async (input, meta) => {\n  return await next(input, { ...meta, signal: derivedSignal });\n};\n```\n\nMiddleware functions are intentionally not contract annotations. Two endpoints can\nshare the same schema and still have different execution policy depending on\nwhere they are served.\n\n## Controller Middleware\n\nController middleware wraps a complete `Controller` and satisfies\n`Middleware<Controller>` from `@emdash/shared/requests`. The seam exists as the\nsanctioned extension point for future cross-cutting concerns (for example\nobservability), even though Wire ships no controller middleware today.\n\nContract validation is not middleware: it is a `createController()` option.\nEvery controller validates by default — `'full'` (inputs + outputs) outside\nproduction, `'inputs'` in production — with an explicit `validate` override:\n\n```ts\nconst controller = createController(api, impl, { validate: 'inputs' });\n```\n\nUse controller middleware for boundary-wide behavior:\n\n- Logging and request instrumentation.\n- Session or authorization checks that apply to every procedure on the served\n  controller.\n\nPrefer handler middleware when the policy belongs to one endpoint. Prefer\ncontroller middleware when the policy belongs to the served boundary.\n\n## Timeout\n\n`withTimeout({ timeoutMs, clock? })` from `@emdash/shared/requests` is handler\nmiddleware backed by Shared `runWithTimeout()` from `@emdash/shared/scheduling`.\nIt derives a child `AbortSignal`, passes that signal to the wrapped handler, and\nconverts an expired deadline to a timeout error.\n\n```ts\nconst startSession = compose(rawStartSession, [\n  withTimeout({ timeoutMs: 10_000, clock }),\n]);\n```\n\nTimeout is infrastructure failure, not a domain result. If callers should handle\na timeout as expected data, model it in the endpoint's `Result` error schema\ninstead.\n\nCaller cancellation still wins over timeout. If the caller aborts the request,\nthe wire call rejects with `CANCELLED`.\n\n## Retry\n\n`withRetry()` from `@emdash/shared/requests` is handler middleware backed by\nShared `retry()` from `@emdash/shared/scheduling`. It requires an explicit\n`shouldRetry` classifier:\n\n```ts\nconst loadRemote = compose(rawLoadRemote, [\n  withRetry({\n    schedule: retrySchedules.fixed(250, 2),\n    shouldRetry: (error) => isWireError(error, 'DISCONNECTED'),\n  }),\n]);\n```\n\nDo not retry arbitrary procedure failures. A retry policy should be attached only\nwhen the operation is idempotent, deduplicated by request identity, or otherwise\nsafe to repeat. Contract mutations already have their own retry behavior and\nretry only `DISCONNECTED` by default.\n\n## Deduplication\n\n`deduplicate(options?)` from `@emdash/shared/requests` is handler middleware for\nsharing one in-flight execution for identical inputs:\n\n```ts\nconst expensiveStats = compose(rawExpensiveStats, [\n  deduplicate({ key: (input) => `${input.repo}:${input.branch}` }),\n]);\n```\n\nThe default key is stable JSON identity. Settled calls are not cached and\nrejections are not cached.\n\nCaller cancellation is independent from shared execution. One caller may stop\nwaiting without aborting the underlying request for other waiters. Set\n`cancelWhenUnused: true` only when it is correct to abort shared work after the\nfinal waiter leaves.\n\n## What Stays Out Of Middleware\n\nKeep protocol-visible behavior in contracts:\n\n- Endpoint kind.\n- Input and output schemas.\n- Upload size and MIME constraints.\n- Live model, log, stream, and job semantics.\n- Mutation idempotency envelopes.\n\nKeep UI presentation policy, such as command visibility or context keys, outside\nWire middleware. Keep domain failures in `Result` payloads when callers are\nexpected to handle them.\n","packages/wire/docs/api/serving.md":"# Serving and Clients\n\nThe API layer turns a contract into a server-side `Controller`, serves it over a\n`WireTransport`, and creates a typed client over the matching transport.\n\n## Creating a Controller\n\n`createController(contract, impl)` maps each endpoint to server behavior:\n\n```ts\nconst controller = createController(\n  notesApi,\n  {\n    session: sessionsHost,\n    activity: () => activityLogServer,\n    clearNotes: (input) => {\n      instance.states.notes.produce((draft) => {\n        draft.notes = [];\n      });\n      return instance.states.notes.snapshot().data;\n    },\n  }\n);\n```\n\nThe `impl` object is keyed by the contract shape. Procedures receive\n`(input, meta)`. Jobs use `{ run, toError? }`, a `LiveJobClientHandle`, or a\n`LiveJobReplicaCache`. Live logs use resolver functions, `LiveLogClientHandle`s, or\n`LiveLogReplicaCache`. Event streams use `createEventStreamHost()`, resolver functions,\nor `EventStreamClientHandle`s. Live model contracts use `expose()` or an existing\n`LiveModelClientHandle`.\n\nMutation idempotency is configured on `expose()`, not on the\ncontroller. The controller only routes calls, snapshots, attachments, and live\nmutation procedure envelopes.\n\nLive model state is separate from the contract because instances are runtime\nresources. A controller can be created once, while conversations, sessions, or\nwindows create and dispose keyed state through `family()` and scoped `expose()`\nlifecycles.\n\nSee the state bridge tests under `src/state/bridge/`.\n\n## Serving\n\n`serve(transport, controller, options?)` listens for protocol messages:\n\n- `call` invokes `controller.call(path, input, meta)`.\n- `snapshot` calls `LiveSource.snapshot()`.\n- `attach` subscribes to a live source and forwards `update` messages.\n- `detach` unsubscribes.\n- `cancel` aborts an in-flight call by id.\n\n```ts\nconst pair = memoryTransportPair();\nconst stop = serve(pair.right, controller, {\n  logger,\n  instrumentation: { callEnd: (event) => logger.debug('call finished', event) },\n});\n```\n\n`serve()` returns an unsubscribe. Call it when the transport or server session\ngoes away. It also aborts in-flight calls and detaches live subscriptions when\nthe transport disconnects or when the serve loop is disposed.\n\n## Validation\n\nContract validation is built into `createController()`. Every controller\nvalidates by default, and the default is environment-sensitive: `'full'`\n(inputs + outputs) everywhere except production, `'inputs'` in production\n(`NODE_ENV === 'production'`). Pass `validate` to override:\n\n```ts\nconst controller = createController(notesApi, impl, { validate: 'inputs' });\nconst stop = serve(pair.right, controller);\n```\n\nPolicies:\n\n- `none`: no schema parsing.\n- `inputs`: parse procedure inputs, upload/download inputs, live model mutation\n  envelopes, job start/cancel inputs, and live topic keys before delegating.\n- `full`: includes `inputs`, then also parses procedure outputs, upload/download\n  results, mutation results, and live job start results.\n\nThe default rule applies identically to bare controllers, component instances,\ncomponent workers, and tests (`createTestWire`), so a contract violation in a\ntest fails the same way it would in dev. Production boundaries stay on\n`inputs`: values crossing a trust boundary are always parsed, while output\nvalidation is skipped where it is too expensive.\n\nValidation covers request/response values that pass through\n`Controller.call()` and live topic keys passed to `Controller.resolveLive()`.\nLive job progress, result, error values, and event stream payloads are emitted\nlater as live updates and are not intercepted.\n\n## Connecting\n\n`connect(transport, options?)` creates a low-level `Connection`:\n\n```ts\nconst connection = connect(pair.left, { instrumentation });\n```\n\n`Connection` supports:\n\n- `call(path, input, { signal? })`.\n- `snapshot(topic)`.\n- `attach(topic, push, { signal?, onReattach? })`.\n- `onDisconnect(cb)`.\n- `dispose()` to release the logical connection without closing its transport.\n\nOn disconnect, pending calls reject with `WireError` code `DISCONNECTED`.\nExisting attachments are retained locally. If the transport exposes\n`onReconnect`, `connect()` re-issues active `attach` requests after the replacement\nlink is live and then calls each attachment's `onReattach` callback.\n\nReplicas use `onReattach` for live models, logs, and jobs to force a fresh\nsnapshot after reattach. Direct client-handle consumers can use the same callback when\nthey need to reseed UI state after reconnect.\n\nAn attachment signal owns establishment for only that caller. Aborting it before readiness cancels\nthe pending attach when no other caller still needs the topic, while shared callers remain attached.\nAfter `attach()` resolves, the returned unsubscribe owns the established interest.\n\n`dispose()` rejects pending calls, releases live attachments and blob channels, and\nunsubscribes the connection from transport events. It intentionally does not call\n`transport.close()`. This allows a short-lived application handshake connection to\nvalidate a candidate transport before handing that same transport to a stable\nreconnecting connection.\n\nThe protocol layer intentionally has no version handshake. Receivers validate the\nmessage `kind` and required fields in `isWireMessage()`; unknown message kinds are\nignored by transport adapters that parse untrusted frames.\n\n## Typed Clients\n\n`client(contract, connection)` returns a `ContractClient` with the same nested\nshape as the contract:\n\n```ts\nconst contractClient = client(notesApi, connection);\n\nconst sessions = createLiveModelReplicaCache(notesApi.session, contractClient.session, {\n  onChange: {\n    notes: (state, meta) => {\n      console.log('notes model:', state, meta.kind);\n    },\n  },\n});\nconst lease = sessions.acquire({ sessionId: 'demo' });\nconst session = await lease.ready();\n\nconst added = await session.mutations.addNote({ text: 'Typed client mutation' });\nawait added.settled;\nawait lease.release();\nawait sessions.dispose();\n```\n\nLive model and live log accessors are client handles. Use `state(key, name)` or\n`handle(key)` to snapshot/attach without local state, or pass the client handle\nto a replica wrapper.\n\nMutations return `ContractMutationInvocation`:\n\n```ts\ntype ContractMutationInvocation<D, E> = {\n  result: LiveMutationResult<D, E>;\n  settled: Promise<void>;\n};\n```\n\n`MutationCallOptions` lets callers provide a `mutationId`, a per-call\n`timeoutMs` deadline override, and an explicit opt-in retry policy. There is no\nautomatic retry; retries reuse the mutation id, so opted-in retries stay\nidempotent:\n\n```ts\nawait session.mutations.addNote({ text: 'Optimistic title' }, {\n  mutationId: 'custom-mutation',\n  retry: { schedule: retrySchedule({ delaysMs: [250, 1_000], maxRetries: 2 }) },\n});\n```\n\n## Cancellation\n\nWire supports cooperative cancellation for request messages. The client sends a\nprotocol message for the request id:\n\n```ts\n{ kind: 'cancel', id: callId }\n```\n\nTyped procedure clients accept an optional `{ signal }` argument:\n\n```ts\nconst abort = new AbortController();\nconst result = client.slowOperation({ id: 'task' }, { signal: abort.signal });\n\nabort.abort();\nawait result; // rejects with WireError code CANCELLED\n```\n\nIf the signal is already aborted, the call rejects locally without posting.\nServer procedure handlers receive the same signal through `CallMeta`:\n\n```ts\nconst controller = createController(api, {\n  slowOperation: async (input, meta) => {\n    await abortableWork(input, meta.signal);\n    return { ok: true };\n  },\n});\n```\n\nCancellation is cooperative. Long-running handlers should pass the signal into\ntheir own async work, listen for `abort`, or periodically check\n`meta.signal?.aborted`. `snapshot` and `attach` requests also share the same server-side\ncancellation registry. Resourced event-stream activation receives an `AbortSignal` so it can release\npartially-created resources when the final pending attachment disappears.\n\nMutations are intentionally not cancellable through this API; they use\n`mutationId` for idempotency and retry. See [mutations](../live/mutations.md).\nCancelling a `liveJob().start` wire call does not cancel the spawned job; job\ncancellation is domain-level through the generated `<path>.cancel` procedure.\n\n## Composing Controllers\n\nMiddle tiers compose by creating a typed client for the upstream contract and\npassing client handles, local handlers, or replicas into a new controller. This\nkeeps forwarding typed and makes the chosen interception points explicit:\n\nIf the middle tier should only relay the entire upstream contract, use\n`forwardController(contract, client)` instead of spelling out every endpoint:\n\n```ts\nconst upstream = client(workspaceApi, connect(upstreamTransport));\nconst controller = forwardController(workspaceApi, upstream);\n```\n\nUse explicit `createController()` composition when the hop needs local behavior:\n\n```ts\nconst upstreamConnection = connect(upstreamTransport);\nconst upstream = client(workspaceApi, upstreamConnection);\n\nconst conversations = createLiveModelReplicaCache(workspaceApi.conversation, upstream.conversation, {\n  lingerMs: 10 * 60_000,\n});\n\nconst controller = createController(workspaceApi, {\n  // Local override.\n  ping: async () => 'desktop-main',\n\n  // Stateless forwarding.\n  git: upstream.git,\n\n  // Stateful interception/cache before serving downstream clients.\n  conversation: conversations,\n});\n```\n\nFor nested contracts, object destructuring is enough to assemble the\nimplementation shape:\n\n```ts\nconst controller = createController(appApi, {\n  ...upstream,\n  settings: {\n    ...upstream.settings,\n    save: async (input, meta) => {\n      await auditSettingsChange(input, meta.signal);\n      return upstream.settings.save(input, meta);\n    },\n  },\n});\n```\n\nUse replicas when the middle tier needs to inspect, cache, or transform live\nstate before exposing it downstream. Use client handles directly when the middle\ntier is only forwarding calls and live sources.\n\n## Multi-Window Sessions\n\n`createWireSessionHub(controller)` serves the same controller to multiple\ntransport sessions:\n\n```ts\nconst hub = createWireSessionHub(controller);\nconst pair = memoryTransportPair();\n\nhub.open('window-1', pair.right);\nconst contractClient = client(api, connect(pair.left));\n```\n\nOpening the same session id closes the previous transport. `close(id)` closes\none session and calls `transport.close?.()` after disposing the serve loop.\n`dispose()` is async: it closes all sessions and awaits `controller.dispose?.()`.\n\n`Controller.dispose`, when present, returns `Promise<void>`. Wrappers such as\nvalidation, logging, session hubs, tests, process runtimes, and Electron exposure\npropagate that promise so LiveJobSource servers and resource hosts can finish shutdown\nbefore their owner reports disposed.\n\n## Server-Side Call Helpers\n\nFor the full middleware pattern, including handler middleware versus controller\nmiddleware, see [composable middleware](./middleware.md).\n\n`compose(target, middlewares)` from `@emdash/shared/requests` applies target-first\nmiddleware arrays to handlers or controllers. The first array entry is outermost:\nit sees the request first and settles last.\n\n```ts\nconst loadStats = compose(\n  async (input, meta) => {\n    return await fetchStats(input.repo, { signal: meta.signal });\n  },\n  [\n    withTimeout({ timeoutMs: 20_000 }),\n    withRetry({ schedule, shouldRetry: isTransient }),\n    withTimeout({ timeoutMs: 5_000 }),\n  ]\n);\n\nconst controller = createController(api, { loadStats });\n```\n\nIn this example the outer timeout bounds the complete retry program, while the\ninner timeout bounds each attempt. Handler middleware must preserve the handler's\nshape. Procedure handlers receive `(input, meta)` and middleware should preserve\nall `meta` fields while deriving a replacement `meta.signal` when it needs\ncooperative cancellation.\n\n`deduplicate(options?)` from `@emdash/shared/requests` wraps procedure\nimplementations to share one in-flight promise for identical inputs:\n\n```ts\nconst controller = createController(api, {\n  expensiveStats: compose(\n    async (input, meta) => {\n      return await loadStats(input.repo, input.branch, { signal: meta.signal });\n    },\n    [deduplicate({ key: (input) => `${input.repo}:${input.branch}` })]\n  ),\n});\n```\n\nBehavior:\n\n- Default key is `stableStringify(input)`, so object property order does not\n  matter.\n- Only in-flight calls are deduplicated. Settled calls are not cached.\n- Rejections are not cached.\n- `meta.signal` is not part of the key and shared execution is not aborted by\n  one caller.\n- Do not wrap mutations; mutation idempotency is handled by `mutationId`.\n\nSee [../../examples/dedupe/server.ts](../../examples/dedupe/server.ts).\n","packages/wire/docs/api/transports.md":"# Transports\n\nA `WireTransport` is the protocol boundary:\n\n```ts\ntype WireTransport = {\n  post(message: WireMessage): void;\n  onMessage(cb: (message: WireMessage) => void): Unsubscribe;\n  onDisconnect(cb: () => void): Unsubscribe;\n  onReconnect?(cb: () => void): Unsubscribe;\n  onTerminalFailure?(cb: (error: unknown) => void): Unsubscribe;\n  close?(): void;\n};\n```\n\nThe same `serve()`, `connect()`, and `client()` code works across every\ntransport. Only construction changes.\n\n`onReconnect` is optional and fires whenever the transport establishes\nconnectivity, including the first time. Its presence marks the transport as\nreconnect-capable: `connect()` then holds calls issued while disconnected until\nthe next reconnect or the call deadline, and re-attaches live topics after each\nreconnect; replicas then force a fresh snapshot. Transports without it fail\ncalls immediately while disconnected. `onTerminalFailure` fires once when the\ntransport gives up permanently; `connect()` rejects all held and future calls\nwith a `WireError('DISCONNECTED')` carrying the cause. `close()` releases\nlisteners registered by the adapter. It closes the underlying channel only when\nthe adapter owns a closeable channel, such as a `MessagePort`.\n\n## Memory\n\n`memoryTransportPair()` creates paired in-process transports for tests and\nexamples:\n\n```ts\nconst pair = memoryTransportPair();\nserve(pair.right, controller);\n\nconst contractClient = client(api, connect(pair.left));\n```\n\n`pair.disconnect()` disconnects both sides. Each endpoint also exposes `close()`,\nwhich aliases the pair disconnect for test cleanup.\n\n`left` and `right` are the two ends of one duplex channel, not semantic roles.\nPosting on `left` delivers to listeners registered on `right`, and posting on\n`right` delivers to listeners registered on `left`. This mirrors\n`MessageChannel`'s `port1`/`port2`: tests commonly call `serve(pair.right, ...)`\nand `connect(pair.left)`, but the opposite assignment is equally valid.\n\n## DOM MessagePort\n\n`domPortTransport(port)` adapts browser `MessagePort` objects:\n\n```ts\nconst channel = new MessageChannel();\nserve(domPortTransport(channel.port1), controller);\n\nconst connection = connect(domPortTransport(channel.port2));\n```\n\nThe adapter calls `port.start?.()` and listens for `message` and `close` events.\n`close()` removes those listeners and calls `port.close?.()`.\n\n## Electron Windows\n\n`exposeWireToWindows()` serves one controller to many Electron renderer windows\nusing `MessageChannelMain`-style ports:\n\n```ts\nconst stop = exposeWireToWindows(\n  {\n    ipcMain,\n    createMessageChannel: () => new MessageChannelMain(),\n  },\n  controller,\n  { channel: 'wire' }\n);\n```\n\nThe renderer asks for a port, then waits for the browser-side transfer:\n\n```ts\nawait requestWirePort({ ipcRenderer, window }, { channel: 'wire' });\nconst port = await awaitWirePort(window, { channel: 'wire' });\nconst contractClient = client(api, connect(domPortTransport(port as MessagePort)));\n```\n\nOpening a new port for the same `webContents.id` closes the old one. Naturally\nclosed ports are removed from the session map. Internally, the helper uses\n`createWireSessionHub(controller)`, so session teardown also closes the transport.\n\n## Node Streams\n\n`streamTransport(input, output)` frames messages as newline-delimited JSON. It is\nuseful for subprocess, stdio, and SSH-style boundaries:\n\n```ts\nconst transport = streamTransport(child.stdout, child.stdin);\nconst contractClient = client(api, connect(transport));\n```\n\nMalformed frames are ignored. `close`, `end`, and `error` on the readable side\ntrigger disconnect listeners. `close()` stops parsing and clears local listeners;\nit does not close the readable or writable streams because those streams are owned\nby the caller.\n\n## Reconnecting\n\n`reconnectingTransport(connectOnce, options?)` wraps an async transport factory:\n\n```ts\nconst transport = reconnectingTransport(\n  async () => {\n    const pair = await openRemoteWirePair();\n    return pair.left;\n  },\n  { backoffMs: [100, 250, 500, 1000] }\n);\n\nawait transport.ready();\n```\n\n`connectOnce` is the readiness boundary for one physical connection. The returned\ntransport remains private until the promise resolves. Applications that require a\nhandshake should open the candidate, run the handshake against it, dispose the\ntemporary logical `Connection`, and only then return the candidate:\n\n```ts\nconst transport = reconnectingTransport(\n  async () => {\n    const candidate = await openRemoteTransport();\n    const handshakeConnection = connect(candidate);\n    const handshakeClient = client(api, handshakeConnection);\n    try {\n      const result = await handshakeClient.initialize({ version: PROTOCOL_VERSION });\n      if (!result.success) throw new ProtocolMismatchError(result.error);\n      return candidate;\n    } catch (error) {\n      candidate.close?.();\n      throw error;\n    } finally {\n      handshakeConnection.dispose();\n    }\n  },\n  {\n    shouldRetry: (error) => !(error instanceof ProtocolMismatchError),\n  }\n);\n\nawait transport.ready();\nconst stableClient = client(api, connect(transport));\n```\n\nThis ordering guarantees that held calls and live-topic reattachments cannot\novertake an application handshake. Wire itself remains version-agnostic; the\napplication owns the handshake contract and permanent-error classification.\n\nThe reconnecting transport does not queue messages. `post()` while no ready\ninner transport exists throws a `WireError('DISCONNECTED')`; robustness is\nowned entirely by `connect()`, which holds calls issued while disconnected\nuntil the next reconnect or the call deadline. When an inner transport\ndisconnects, listeners are notified, readiness resets, and reconnection starts.\n\n`ready()` resolves after the current `connectOnce` succeeds and its candidate is\ninstalled. After a disconnect, a new call to `ready()` waits for the replacement\ngeneration. A previously resolved readiness promise does not become pending\nagain, so disconnect observers should call `ready()` after receiving the\ndisconnect event.\n\n`shouldRetry(error, { attempt, isReconnect })` classifies factory or handshake\nfailures. Returning `false`, or exhausting a finite `retrySchedule`, permanently\nstops that transport, rejects its current readiness promise, and fires\n`onTerminalFailure` with the terminal error. Future `post()` calls fail with the\nterminal error. By default, failures remain retryable under the configured\nschedule.\n\n`onReconnect` fires whenever an inner transport reaches readiness, including\nthe first one. `Connection` listens for that signal to flush held calls and\nre-issue active `attach` requests; replicas refresh their snapshots after\nreattach. `close()` stops the retry loop, closes the current inner transport if\npresent, fires `onTerminalFailure`, and clears listeners.\n\n## Composition Notes\n\n`reconnectingTransport()` accepts `clock` and `retrySchedule` options. The legacy\n`backoffMs` array is normalized to a repeat-last retry schedule. Closing the\ntransport synchronously aborts pending reconnect sleeps and disposes late\nconnections.\n\nElectron window exposure returns an async cleanup function. Await it when tearing\ndown a runtime host so session hubs and controllers finish disposal before the\nworker or window bridge is replaced.\n\nTransport composition is ordinary function wrapping, so order matters when\nstacking adapters around `reconnectingTransport()`. For semantic telemetry (one\nevent per logical call, snapshot, attachment, cancellation, resync, or mutation\ndedupe), attach a `WireInstrumentation`; see\n[observability](../observability.md).\n","packages/wire/docs/live/event-stream.md":"# Event Streams\n\n`eventStream({ key, event })` exposes a keyed server-to-client event channel for\nloss-tolerant notifications. It reuses the wire live attachment protocol but does\nnot retain events or expose meaningful snapshots.\n\nUse event streams when subscribers can recover from missed events by resyncing\nfrom another source of truth, such as a file tree, cache, or database query. Use\n`liveLog` for retained append-only text and `liveModel` for convergent state.\n\n## Contract\n\n```ts\nconst api = defineContract({\n  fileEvents: eventStream({\n    key: z.object({ rootPath: z.string() }),\n    event: z.object({\n      kind: z.enum(['create', 'update', 'delete']),\n      path: z.string(),\n    }),\n  }),\n});\n```\n\nThe key addresses an event stream instance. The event schema types the\nclient-side `onEvent` callback and documents the payload shape.\n\n## Server\n\nMost servers use `createEventStreamHost()`:\n\n```ts\nconst fileEvents = createEventStreamHost(api.fileEvents);\n\nconst controller = createController(api, {\n  fileEvents,\n});\n\nfileEvents.emit(\n  { rootPath: '/repo' },\n  { kind: 'update', path: '/repo/package.json' }\n);\n```\n\n`emit()` is fire-and-forget. If no client is attached to that key, the event is\ndropped. When the last subscriber detaches, the host discards the keyed source.\n\nIf attachment presence owns an external resource, pass lifecycle hooks to the\nhost. `onActive` runs when the first subscriber attaches to a key, and `onIdle`\nruns when the last subscriber detaches:\n\n```ts\nconst resourceEvents = resourcedStream({\n  key: z.object({ rootPath: z.string() }),\n  event: z.object({ kind: z.string(), path: z.string() }),\n});\nconst fileEvents = createEventStreamHost(resourceEvents, {\n  onActive(key) {\n    void startWatcher(key);\n  },\n  onIdle(key) {\n    void stopWatcher(key);\n  },\n});\n```\n\nThis is useful for resources that should be recreated after reconnect. The wire\nclient automatically reattaches live topics after reconnect, so `onActive` runs\nagain in the new server process.\n\nWhen a successful attach must mean that the resource is ready, define the endpoint with\n`resourcedStream()` and provide `activate`. Activation receives a signal and resolves to its owned\ndisposer:\n\n```ts\nconst fileEvents = createEventStreamHost(api.fileEvents, {\n  async activate(key, signal) {\n    const watcher = await startWatcher(key, { signal });\n    return () => watcher.dispose();\n  },\n});\n```\n\nConcurrent subscribers share one activation. The host aborts pending activation when the final\nsubscriber leaves, disposes a late result, and keeps activation alive when another subscriber still\nowns the key.\n\nYou can also pass a resolver to `createController()` if another component owns\nthe source:\n\n```ts\ncreateController(api, {\n  fileEvents: (key) => sources.get(key.rootPath),\n});\n```\n\n## Client\n\n```ts\nconst unsubscribe = await client.fileEvents.subscribe(\n  { rootPath: '/repo' },\n  {\n    onEvent(event) {\n      console.log(event.kind, event.path);\n    },\n    onGap() {\n      void reloadFileTree();\n    },\n    onError(error, { retrying }) {\n      if (!retrying) showFileEventStreamError(error.message);\n    },\n    signal,\n  }\n);\n```\n\n`onGap` runs after the underlying transport reattaches. Events may have been lost\nwhile the link was disconnected or while a supervised subprocess restarted, so\nconsumers should refresh any derived state in that callback.\n\nInitial attachment does not call `onGap`; subscribers should perform their own\ninitial load before or after subscribing.\n\nThe `subscribe()` promise rejects if the initial attachment fails. After an\nattachment is established, `DISCONNECTED` reattach failures call `onError` with\n`retrying: true` and remain registered for a later reconnect. Non-retryable\nreattach errors call `onError` with `retrying: false` and terminate the\nattachment.\n\n## Semantics\n\n- Events are at-most-once and only delivered to currently attached clients.\n- No historical events are retained for late subscribers.\n- Event stream keys are validated by controller validation.\n- Event payloads are not validated on the push path, matching other live updates.\n- Forwarding preserves upstream gaps and reattach errors even when the downstream\n  transport remains connected.\n\n## Example\n\nSee [../../examples/event-stream/client.ts](../../examples/event-stream/client.ts)\nfor a runnable in-process example.\n","packages/wire/docs/live/live-job.md":"# Live Jobs\n\nLive jobs model async work as a live state machine. They are useful when callers\nneed both a final result and a stream of progress updates.\n\n## State Shape\n\n`liveJobStateSchema(progress, result, error)` creates a schema for:\n\n```ts\ntype LiveJobState<P, R, E> =\n  | { status: 'running'; startedAt: number; progress: P[]; progressCount: number }\n  | { status: 'succeeded'; startedAt: number; finishedAt: number; progress: P[]; result: R }\n  | { status: 'failed'; startedAt: number; finishedAt: number; progress: P[]; error?: E; cause?: SerializedError }\n  | { status: 'cancelled'; startedAt: number; finishedAt: number; progress: P[] };\n```\n\n`progress` is a retained ring of recent entries. `progressCount` is the total\nnumber of progress events emitted, including entries that may have rolled out of\nthe retained ring. Terminal states retain lifecycle timestamps and the most\nrecent progress entries, but they do not carry `progressCount`.\n\n## Server\n\n`LiveJobSource<I, P, R, E>` accepts a handler that returns `Result<R, E>`:\n\n```ts\nconst jobs = new LiveJobSource<Input, Progress, Result, ErrorState>(\n  async (input, ctx) => {\n    ctx.progress({ step: 'checkout' });\n    await build(input, ctx.signal);\n    ctx.progress({ step: 'package' });\n    return ok({ artifact: `${input.target}.zip` });\n  },\n  { toError: (error) => ({ message: error instanceof Error ? error.message : String(error) }) }\n);\n\nconst { jobId } = jobs.start({ target: 'desktop' });\n```\n\nOptions:\n\n- `generation`: optional fixed generation for every job state model.\n- `terminalRetainMs`: how long terminal state remains attachable. Default:\n  `LIVE_JOB_TERMINAL_RETAIN_MS` (5 minutes).\n- `idFactory`: custom job id factory, useful for deterministic tests.\n- `clock`: custom timestamp source, useful for deterministic tests.\n- `toError`: optional fallback for unexpected thrown errors. Domain failures\n  should be returned as `err(error)` from the handler.\n- `onRunStarted`, `onRunChanged`, and `onRunEvicted`: optional hooks for callers\n  that want to maintain their own job listing.\n\nHandlers receive `LiveJobContext`:\n\n- `ctx.progress(value)` appends retained progress and emits an update.\n- `ctx.signal` is an `AbortSignal`. If the signal is aborted, the job finishes\n  as `cancelled`.\n\nReturn `ok(result)` for successful completion and `err(error)` for expected\ndomain failures. If the handler throws and `toError` is configured, the thrown\nvalue is mapped into typed `error`; otherwise the failed state carries a\nserialized `cause`.\n\n`source(jobId)` returns a read-only live source for that job. `snapshot(jobId)` and\n`getState(jobId)` are convenience accessors. `cancel(id)` aborts a running job.\n`dispose()` is async: it aborts running jobs, waits for cooperative handlers to\nsettle, clears eviction timers, and removes all job state.\n\nTerminal job state is retained for `LIVE_JOB_TERMINAL_RETAIN_MS` (5 minutes) so\nlate clients can reattach by id. This retention is process-local, not durable.\n\n## Consumers\n\nConsumers normally reach jobs through the API layer. The contract client starts,\ncancels, and attaches to jobs without retaining local state:\n\n```ts\nconst { jobId } = await contractClient.build.start({ target: 'desktop' });\nconst handle = contractClient.build.handle(jobId);\n\nconst detach = await handle.attach((update) => {\n  console.log(update.delta);\n});\n\nawait contractClient.build.cancel(jobId);\ndetach();\n```\n\nUse `createLiveJobReplicaCache()` when a consumer wants a `result` promise, progress\ncallbacks, ref-counted attachment, and terminal-state retention. If the job fails,\n`ReplicaJob.result` rejects with `LiveJobFailedError`; if it is cancelled, it\nrejects with `LiveJobCancelledError`. Progress emitted by a seed is suppressed so\nreattaching clients do not replay old progress as fresh events.\n\n## Contract Endpoint\n\nThe API contract layer can expose a job directly:\n\n```ts\nconst api = defineContract({\n  build: liveJob({\n    input: z.object({ target: z.string() }),\n    progress: z.object({ step: z.string() }),\n    result: z.object({ artifact: z.string() }),\n    error: z.object({ message: z.string() }),\n  }),\n});\n```\n\nOn the server, implement the endpoint with `{ run, toError? }`:\n\n```ts\nconst controller = createController(api, {\n  build: {\n    run: async (input, ctx) => {\n      ctx.progress({ step: 'compile' });\n      return ok({ artifact: `${input.target}.zip` });\n    },\n    toError: (error) => ({\n      message: error instanceof Error ? error.message : String(error),\n    }),\n  },\n});\n```\n\nThe typed client exposes a live job client handle. Use it directly for\nlow-level forwarding, or wrap it in `createLiveJobReplicaCache()` when a consumer wants\nref-counted local state:\n\n```ts\nconst jobs = createLiveJobReplicaCache(api.build, contractClient.build, { lingerMs: 30_000 });\nconst lease = await jobs.start({ target: 'desktop' });\nconst handle = await lease.ready();\n\nhandle.onProgress((progress) => console.log(progress.step));\nconsole.log(await handle.result);\n\nawait lease.release();\n\nconst reattachedLease = jobs.acquire(handle.jobId);\nconst reattached = await reattachedLease.ready();\nconsole.log(await reattached.result);\nawait reattachedLease.release();\nawait jobs.dispose();\n```\n\n`ReplicaJob` exposes `jobId`, `ready`, `result`, `getState()`,\n`onProgress(cb)`, and `cancel()`. The `LiveJobReplicaCache` manager owns ref counting,\nsubscription sharing, and terminal-state retention.\n\n`LiveJobSource` accepts a `Clock` for deterministic start/finish timestamps and\nterminal retention eviction. Server disposal is awaitable: it cancels active job\nruns, waits for cooperative handlers to settle, and disposes retention timers\nbefore returning.\n\nCancellation at the procedure/wire level is documented in\n[serving](../api/serving.md#cancellation). Job cancellation is domain-level:\n`ReplicaJob.cancel()` calls the generated `<path>.cancel` procedure, which calls\n`LiveJobSource.cancel(jobId)`.\n\nSee [../../examples/job-contract/client.ts](../../examples/job-contract/client.ts).\n","packages/wire/docs/live/live-log.md":"# Live Logs\n\nLive logs are append-only text streams with retained tail snapshots. They are a\ngood fit for terminal output, build logs, and agent process output where clients\nneed to attach late and still see recent context.\n\n## Server\n\n`LiveLogSource` stores a bounded text buffer. `append(chunk)` emits\n`{ chunk }` deltas to subscribers and updates the retained snapshot:\n\n```ts\nconst server = new LiveLogSource({ generation: 3000, maxBufferBytes: 12 });\n\nexport function appendLine(line: string): void {\n  server.append(`${line}\\n`);\n}\n```\n\nOptions:\n\n- `generation`: optional fixed generation, useful in tests.\n- `maxBufferBytes`: retained buffer size. The default is 1 MiB.\n\nSnapshots contain `LiveLogSnapshotData`:\n\n```ts\ntype LiveLogSnapshotData = {\n  baseOffset: number;\n  text: string;\n  truncated: boolean;\n};\n```\n\n`baseOffset` is the byte offset of `text` in the logical full log. When the\nserver drops old bytes because `maxBufferBytes` was exceeded, `truncated` is\n`true`. The server keeps at least the newest chunk even if it exceeds the byte\nlimit.\n\n`reseed()` starts a new generation and resets sequence to `0`. With no argument,\nit clears retained text and resets `baseOffset` to `0`; with retained snapshot\ndata, it seeds the buffer from that data.\n\n## Consumers\n\nConsumers normally reach logs through the API layer. The client handle is useful\nwhen you want to stream directly into a terminal or log viewer:\n\n```ts\nconst output = contractClient.activity.handle(session);\nconst snapshot = await output.snapshot();\nterm.write(snapshot.data.text);\n\nconst detach = await output.attach((update) => {\n  term.write((update.delta as { chunk: string }).chunk);\n});\n```\n\nUse `createLiveLogReplicaCache()` when a process wants a retained local buffer that can\nalso be served downstream. Without a custom sink, the replica keeps an eager\nbounded `LiveLogSource` buffer and `text()` remains readable. With a custom `LogSink`,\nthe sink owns storage; readable `LogStore`s add `text()`, while write-only sinks\ncan stream directly into xterm without a duplicate text buffer.\n\nThe replica's internal log follower tracks the followed generation/sequence\nplus the UTF-8 byte offset already written to its sink. On reconnect or explicit `refresh()`, it\nsplices from the retained snapshot when the generation still matches, appending\nonly missing bytes instead of resetting rendered output. Generation changes or\nevicted gaps still reset the sink from the retained snapshot.\n\nThe underlying protocol follower resyncs on update-before-seed, generation\nmismatch, sequence gap, or invalid log delta. Resync events use the same `resync`\ninstrumentation hook as live models.\n\n## API Layer Usage\n\nContracts expose logs with `liveLog({ key })`:\n\n```ts\nconst api = defineContract({\n  activity: liveLog({ key: sessionKeySchema }),\n});\n\nconst controller = createController(api, {\n  activity: () => activityLogServer,\n});\n\nconst activity = contractClient.activity.handle(session);\nconst detach = await activity.attach((update) => {\n  console.log((update.delta as { chunk: string }).chunk);\n});\n\ndetach();\n```\n\nThe API layer handles topic encoding, snapshots, attachment, and detachment.\n\nSee [../../examples/live-log/client.ts](../../examples/live-log/client.ts).\n","packages/wire/docs/live/live-model.md":"# Live States and Protocol\n\nLive states are reactive JSON state containers. A server owns authoritative\nstate, emits ordered Immer patches, and clients apply those patches locally.\nWhen a client detects a gap, it refetches a snapshot and resumes from the new\ngeneration.\n\n## Protocol Terms\n\nThe shared protocol lives in `src/live/protocol`.\n\n- `LiveSnapshot<T>` is the full state at a cursor:\n  `{ generation, sequence, timestamp, data }`.\n- `LiveUpdate` is an ordered delta:\n  `{ generation, baseSequence, sequence, timestamp, delta, mutationIds? }`.\n- `LiveCursor` is `{ generation, sequence }` and identifies a point in a live\n  state stream.\n- `LiveSource` is the common server-side shape consumed by the API layer:\n  `snapshot()` plus `subscribe(cb)`.\n\n`generation` changes when the server calls `reseed()`. `sequence` increments for\neach effective patch in a generation. `baseSequence` must equal the client's\ncurrent sequence; otherwise the client has missed an update and resyncs from\n`snapshot()`.\n\n## Server\n\n`LiveStateSource<T>` owns one authoritative state object. Mutate it with\n`produce()`, not by mutating the original object:\n\n```ts\nconst server = new LiveStateSource<TaskListState>(\n  {\n    tasks: [{ id: 'task-1', title: 'Read the plan', done: false }],\n    filter: 'all',\n  },\n  1000\n);\n\nconst cursor = server.produce(\n  (draft) => {\n    draft.tasks.push({ id: 'task-2', title: 'Apply the first patch', done: false });\n  },\n  { mutationIds: ['example-add-task'] }\n);\n```\n\n`produce()` returns the cursor containing the change. If the mutator is a no-op,\nno update is emitted and the current cursor is returned. `snapshot()` deep\nclones the current state. `reseed(next?)` replaces the generation, resets\nsequence to `0`, optionally replaces state, and forces clients to resync on the\nnext observed update.\n\n## Consumers\n\nConsumers normally reach live states through a live model contract. Use the\ncontract client when you only need protocol access:\n\n```ts\nconst model = contractClient.conversation.state({ sessionId }, 'state');\nconst snapshot = await model.snapshot();\nconst detach = await model.attach((update) => {\n  console.log(update.delta);\n});\n```\n\nWrap that live state client handle in `createLiveModelReplicaCache()` when a process wants local\nstate, mutation settling, ref counting, or a downstream `LiveSource`; see\n[replicas](./replicas.md#model-replicas).\n\nThe package also has low-level protocol followers internally. They consume\nsnapshots and ordered updates, then resync when:\n\n- an update arrives before `seed()`.\n- `generation` differs from the local generation.\n- `baseSequence` differs from the local sequence.\n- Immer patch application throws.\n- schema validation fails in non-production builds.\n\nThe reported resync reasons are `generation`, `sequence-gap`, `patch-failed`,\nand `validation`. Schema validation is skipped when `NODE_ENV === 'production'`;\nthe generation and sequence checks are the primary correctness mechanism.\n\nEvery follower is constructed with a required resync failure policy that\ndecides what happens when the snapshot refetch itself fails. Wire ships two\ncanned policies: `resyncRetry({ schedule? })` retries on a bounded backoff\nuntil success or dispose, and `resyncMarkStale()` gives up while keeping\nstaleness observable (`stale` on the client) until the next update or reattach\ntriggers a fresh attempt. Production replicas use `resyncRetry`. Every failure\nfires the `resyncFailed` instrumentation event. `refresh()` resolves only when\nthe follower is fresh: concurrent calls coalesce onto the in-flight resync and\na trigger arriving mid-reseed re-runs the loop before resolution. Disposing a\nfollower-based client rejects all of its pending waiters — including\nwait-forever waiters (`timeoutMs <= 0`) — with a disposed error.\n\n## Cursor and Mutation Waiters\n\n`waitForCursor(cursor, timeoutMs?)` resolves when the client has reached a\ncursor. `waitForMutation(mutationId, timeoutMs?)` resolves when an applied update\ncontains the mutation id, or when a seed/resync lands because the snapshot is\nauthoritative.\n\nThese waiters are used by mutation settling; see [mutations](./mutations.md).\n\nSee [../../examples/state-kernel/client.ts](../../examples/state-kernel/client.ts)\nfor an end-to-end example.\n","packages/wire/docs/live/mutations.md":"# Mutations\n\nMutations connect API calls to live model updates. A mutation can update one\nmodel, many instances of one model, or several model refs. The key extra piece\nis the `mutationId`: it tags emitted `LiveUpdate`s so clients can prove their\nbound live models have observed the mutation.\n\n## Why Mutation IDs Exist\n\nAn RPC result only tells the caller that the server handler finished. It does\nnot prove every live subscription in the UI has applied the corresponding\npatches. Mutation ids bridge that gap:\n\n```ts\nserver.produce(\n  (draft) => {\n    draft.tasks.push({ id: 'task-2', title: 'Apply the first patch', done: false });\n  },\n  { mutationIds: ['example-add-task'] }\n);\n```\n\nThe update carries `mutationIds: ['example-add-task']`. A replica can resolve\n`waitForMutation('example-add-task')` when its local model applies that update.\n\n## Providers and Context\n\nLive model contract mutations run against a `LiveModelProvider` — most commonly\none produced by `expose()` from `@emdash/wire/state`, which bridges kernel state\nonto the endpoint. Mutation handlers update kernel cells and await the observed\nrevision so the returned cursor is settled:\n\n```ts\nconst sessions = expose(api.session, { metadata: (key) => metadataCell(key) }, {\n  mutations: {\n    async setTitle(ctx) {\n      const revision = metadataCell(ctx.key).update(\n        (previous) => ({ ...previous, title: ctx.input.title }),\n        { mutationIds: [ctx.mutationId] }\n      );\n      await ctx.observed('metadata', revision);\n      return ok({ title: ctx.input.title });\n    },\n  },\n});\n```\n\nKeys use `stableStringify()`, so object key order does not matter when providers\nand client bindings look up an instance. The mutation context is instance-bound:\n`ctx.key` identifies the addressed member and `ctx.observed(name, revision)`\nrecords the cursor of every touched member state. The wire result is:\n\n```ts\ntype LiveMutationResult<D, E> =\n  | { success: true; data: { data: D; cursors: LiveCursorEntry[] } }\n  | { success: false; error: E };\n```\n\nThe `data.data` value is the domain result. `data.cursors` tells the client which\nlive model bindings need to catch up.\n\n## Client Settling\n\n`LiveModelReplicaCache.acquire(key)` returns a `ReplicaInstance` for one group key. Its\nmutation methods call the live model client handle and then settle against the local\n`ReplicaState`s.\n\nGroup mutation methods return `{ result, settled }`:\n\n```ts\nconst sessions = createLiveModelReplicaCache(api.session, contractClient.session);\nconst lease = sessions.acquire({ sessionId: 'demo' });\nconst session = await lease.ready();\n\nconst added = await session.mutations.addNote({ text: 'Typed client mutation' });\nawait added.settled;\nawait lease.release();\nawait sessions.dispose();\n```\n\n`settled` waits for every cursor in the mutation result. For each cursor entry,\nit resolves when either:\n\n- the matching binding applies an update tagged with the mutation id, or\n- the matching binding reaches the returned cursor.\n\nThis lets UI code safely read live client snapshots after `await settled`.\n\n## Group Contract Mutations\n\nThe API layer integrates mutations through `liveModel()` member\nmutations. Each group mutation becomes a client method:\n\n```ts\nconst updated = await session.mutations.setTitle({ title: 'Grouped wire' }, {\n  mutationId: 'custom-mutation',\n});\nawait updated.settled;\n```\n\nIf no id is provided, the replica mutation helper generates one with\n`createMutationId()`.\nExplicit ids are useful for optimistic previews, where the preview and server\nmutation must share the same confirmation id.\n\n## Idempotency and Retries\n\nThe server-side idempotency cache used by `expose()` stores settled mutation\nresults by `mutationId` and shares one in-flight execution for concurrent\nduplicates.\n\nBy default the cache keeps entries for 5 minutes with a 1000-entry cap.\nConfigure or disable it through the `idempotency` option:\n\n```ts\nconst sessions = expose(api.session, states, {\n  mutations,\n  idempotency: { ttlMs: 60_000, maxEntries: 500 },\n});\n\nconst withoutDedupe = expose(api.session, states, { mutations, idempotency: false });\n```\n\nThe client never retries mutations automatically. Callers opt in per call with\nan explicit retry schedule; retries reuse the same `mutationId`, so the\nserver-side cache deduplicates them:\n\n```ts\nawait session.addNote(input, {\n  mutationId: 'add-note-1',\n  retry: { schedule: retrySchedule({ delaysMs: [250, 1_000], maxRetries: 2 }) },\n});\n```\n\nOpted-in retries fire only for `DISCONNECTED` and `TIMEOUT` errors; they never\nhappen for `CANCELLED` errors.\n\nThe cache is process-local and temporary. It provides at-most-once behavior\nwithin one server process lifetime, not durable exactly-once semantics. If a\nmutation has durable side effects such as database writes, store the\n`mutationId` in that domain layer too.\n\nUse `procedure()` for API calls that do not need live model cursor settling.\n`mutation()` is only valid as a member of `liveModel().mutations`\nin the contract API.\n\n","packages/wire/docs/live/replicas.md":"# Replicas\n\nReplicas are consumer-instantiated materialization wrappers around client handles from\n`client()`. The typed client itself never stores live state. A consumer chooses\none of four shapes:\n\n- Own state: `LiveStateSource`, `LiveLogSource`, `LiveJobSource`, or an\n  `expose()` provider over kernel state.\n- Stream directly: use a client `snapshot()`/`attach()` handle with no local store,\n  for example PTY output written straight into xterm.\n- Forward: pass client handles or subtrees to `createController()` so a hop stays stateless.\n- Replica: wrap a client handle to hold local state, share upstream subscriptions, and\n  serve downstream clients.\n\nEvery replica manager has the same lifecycle shape:\n\n```ts\nconst lease = replica.acquire(key);\nconst instance = await lease.ready();\nawait lease.release();\nawait replica.dispose();\n```\n\n`acquire()` increments the ref count for that key. The first lease opens the\nupstream subscription, concurrent leases share it, and the last release starts\nthe optional `lingerMs` timer. `peek(key)` returns a warm instance while it is\nretained.\n\n## Model Replicas\n\nLive models are exposed only through `liveModel()`. A\n`LiveModelReplicaCache` follows a live model client handle and yields a `ReplicaInstance`:\n\n```ts\nimport { createImmutableMobxStore } from '@emdash/wire/mobx';\n\nconst conversations = createLiveModelReplicaCache(api.conversation, contractClient.conversation, {\n  lingerMs: 30_000,\n  stores: {\n    state: () => createImmutableMobxStore(),\n  },\n  onChange: {\n    state: (value, meta) => console.log(value, meta.kind),\n  },\n});\n\nconst lease = conversations.acquire({ conversationId: 'demo' });\nconst conversation = await lease.ready();\n\nconst updated = await conversation.mutations.setTitle({ title: 'Replicated' });\nawait updated.settled;\nconsole.log(conversation.states.state.current());\n```\n\n`ReplicaInstance.states` contains one `ReplicaState` per contract member.\n`ReplicaState` follows upstream snapshots and updates, stores current state in a\npluggable `StateStore`, and re-emits updates in a replica-local cursor space. If\nno store is supplied, the replica uses the plain immutable store.\nMobX consumers can pass stores per liveState through `stores`. Use\n`createImmutableMobxStore()` for snapshot/reference consumers such as\n`useSyncExternalStore`, or `createReactiveMobxStore()` for `observer` components\nthat benefit from property-level MobX tracking.\n\nMutation helpers return `{ result, settled }`. On success, the replica translates\nupstream cursors to local cursors before returning them to downstream clients, so\nboth local UI and served clients settle against the same local state.\n\nA cursor-translation timeout never fails a committed mutation. If the replica\ncannot observe the mutation's upstream cursor in time, the mutation still\nresolves successfully and the result data carries `settled: false` (client-side\nmetadata; the wire shape of `LiveMutationResult` is unchanged). Instrumentation\nrecords the timeout via `cursorTranslationTimeout`, and the `settled` promise\nstays best-effort as before.\n\n## Log Replicas\n\nUse a `LiveLogReplicaCache` when a process needs a local retained text buffer or wants\nto serve log output downstream:\n\n```ts\nimport { createMobxLogStore } from '@emdash/wire/mobx';\n\nconst outputs = createLiveLogReplicaCache(api.ptyOutput, contractClient.ptyOutput, {\n  lingerMs: 10_000,\n  maxBufferBytes: 1024 * 1024,\n  store: () => createMobxLogStore(),\n});\n\nconst lease = outputs.acquire({ sessionId });\nconst output = await lease.ready();\noutput.onAppend((chunk) => index(chunk));\nconsole.log(output.text());\n```\n\nWithout a custom sink, the log replica keeps an eager internal `LiveLogSource` buffer\nthat is readable through `text()` and can serve downstream clients. With a custom\nsink, the sink owns storage: readable `LogStore`s expose `text()`, while\nwrite-only `LogSink`s (for example xterm scrollback) support reset/append without\nallocating a duplicate string buffer.\n\nFor terminal rendering, prefer the client handle directly:\n\n```ts\nconst output = contractClient.ptyOutput.handle({ sessionId });\nterm.write((await output.snapshot()).data.text);\nconst detach = await output.attach((update) => {\n  term.write((update.delta as { chunk: string }).chunk);\n});\n```\n\n## Job Replicas\n\n`LiveJobReplicaCache` wraps a live job client handle. It forwards `start()` and `cancel()`,\nmaterializes job state by `jobId`, and keeps terminal state readable under lease\nor retention:\n\n```ts\nimport { createImmutableMobxStore } from '@emdash/wire/mobx';\n\nconst jobs = createLiveJobReplicaCache(api.build, contractClient.build, {\n  lingerMs: 30_000,\n  store: () => createImmutableMobxStore(),\n});\n\nconst lease = await jobs.start({ target: 'desktop' });\nconst job = await lease.ready();\njob.onProgress((progress) => console.log(progress.step));\nconsole.log(await job.result);\n\nawait lease.release();\n\nconst late = jobs.acquire(job.jobId);\nconsole.log((await late.ready()).getState());\n```\n\nWhen a job reaches `succeeded`, `failed`, or `cancelled`, the replica detaches\nfrom the upstream live topic but retains the local terminal state while leases or\n`lingerMs` keep it warm.\nThe job replica also supports a pluggable reset/current `JobStore`, so\n`getState()` can be observable. The internal `LiveStateSource` used for downstream\nserving is created only if `snapshot()` or `subscribe()` is requested.\n\n## Serving Replicas\n\nReplicas can be passed directly into `createController()`:\n\n```ts\nconst upstream = client(api, connect(sshTransport));\n\nconst controller = createController(api, {\n  conversation: createLiveModelReplicaCache(api.conversation, upstream.conversation),\n  ptyOutput: upstream.ptyOutput, // forward bytes without buffering\n  build: createLiveJobReplicaCache(api.build, upstream.build),\n});\n```\n\nUse replicas only when the hop needs local state. If the hop is a pure relay,\nforward the client subtree instead.\n","packages/wire/docs/observability.md":"# Observability\n\n`@emdash/wire` exposes observability at the same boundaries that own runtime\nbehavior: server calls, client calls, live topic attachment, mutation dedupe,\nlive-client resyncs, transport messages, and scope cleanup.\n\n## Ambient Logger\n\nThe logger interface and redaction utilities live in `@emdash/shared/logger`.\nWire uses the shared ambient logger to attach call context without passing a\nlogger through every domain function:\n\n```ts\nimport { log, runWithLogger } from '@emdash/shared/logger';\n\nawait runWithLogger(logger.child({ requestId: 'r1' }), async () => {\n  log.info('handling request');\n});\n```\n\nNode entry points can install the `AsyncLocalStorage` store:\n\n```ts\nimport { installAsyncLogContext } from '@emdash/shared/logger/node';\n\ninstallAsyncLogContext();\n```\n\nBrowser and renderer code use a synchronous fallback. It still supports a root\nlogger and scoped synchronous blocks, but it does not preserve context across\n`await`.\n\n## Instrumentation Hooks\n\nUse `WireInstrumentation` (exported from `@emdash/wire/rpc`) for typed events\nthat can be adapted to logs, metrics, or tracing. The seam is threaded through\nthe public options of `serve()`, `connect()`, replicas, and `expose()`:\n\n```ts\nimport type { WireInstrumentation } from '@emdash/wire/rpc';\n\nconst instrumentation: WireInstrumentation = {\n  callEnd: (event) => logger.debug('call finished', event),\n  resyncFailed: (event) => logger.warn('resync failed', event),\n};\n\nserve(transport, controller, { logger, instrumentation });\nconst connection = connect(clientTransport, { instrumentation });\nconst contractClient = client(api, connection);\n```\n\nHooks currently cover:\n\n- procedure start/end, cancellation, and duration.\n- snapshot timing and errors.\n- live topic attach/detach.\n- live model/log resync reasons and resync failures.\n- replica cursor-translation timeouts.\n- mutation dedupe hits.\n- scope cleanup errors.\n- transport connect/disconnect events.\n\nThere are no bundled logger adapters: adapt the typed events to your logging or\nmetrics stack at the application edge.\n\nServing and client options are documented in [API serving](./api/serving.md).\nTransport construction is documented in [API transports](./api/transports.md).\n\n## Scope Logging\n\n`createScope({ logger, label })` attaches a logger to the scope tree. Children\ninherit the logger with a `scope` binding containing their label path:\n\n```ts\nconst runtimeScope = createScope({ label: 'runtime', logger });\nconst sessionScope = runtimeScope.child('session:abc');\n\nsessionScope.log.info('session started');\n```\n\nCleanup errors and failed runs are reported through the scope logger by default.\nUse `describeScope(scope)` for a lightweight label tree when debugging retained\nresources or stuck async work. Scope descriptions include active run labels,\nstart times, and cancellation state. When the scope is created with a custom\n`Clock`, those start times come from that clock, which keeps lifecycle diagnostics\ndeterministic in tests.\n\nScope lifecycle behavior is documented in [runtime lifecycle](./runtime/lifecycle.md)\nand [structured concurrency](./runtime/structured-concurrency.md).\n","packages/wire/docs/runtime/components.md":"# Components\n\n`WireComponent` is the authored abstraction for runtime and service contracts that can be created\nin-process or served from a worker process.\n\nUse a component when a contract has a reusable runtime implementation and the composition root should\nchoose whether it runs in the current process or in a supervised worker. A component is not a\ndependency injection container: it describes one contract implementation, its explicit requirements,\nits config schema, and how to synchronously create a scope-owned instance.\n\n## Define A Component\n\n```ts\nimport { defineContract, procedure, createController } from '@emdash/wire/rpc';\nimport { defineWireComponent, requireContract } from '@emdash/wire/worker';\nimport { z } from 'zod';\n\nexport const clockContract = defineContract({\n  now: procedure({ input: z.void().optional(), output: z.number() }),\n});\n\nexport const counterContract = defineContract({\n  increment: procedure({ input: z.void().optional(), output: z.number() }),\n});\n\nconst component = defineWireComponent({\n  id: 'counter',\n  contract: counterContract,\n  requirements: {\n    clock: requireContract(clockContract),\n  },\n  configSchema: z.object({\n    initialValue: z.number().int().default(0),\n  }),\n  create: ({ scope, dependencies, config, instance }) => {\n    let value = config.initialValue;\n    scope.add(() => {\n      value = 0;\n    });\n\n    return instance({\n      scope,\n      controller: createController(counterContract, {\n        increment: async () => {\n          await dependencies.clock.now(undefined);\n          value += 1;\n          return value;\n        },\n      }),\n    });\n  },\n});\n```\n\n`create` is synchronous. Expected async work belongs in domain procedures, `LiveJobSource`s, machines,\nlive state, or other explicit runtime APIs. Directory creation, installs, probing, and startup\noperations should expose their own lifecycle and errors rather than hiding inside component\nconstruction.\n\n`instance({ scope, controller })` adapts the controller into a typed in-process client using a memory\ntransport. The controller should be unvalidated; the component creation boundary applies validation\naccording to the caller's `validate` policy.\n\n## Explicit Requirements\n\nRequirements are plain object keys. A component can require another Wire contract with\n`requireContract(contract)`. Deployment values such as paths, limits, executable names, and feature\nflags belong in typed component config, not dependency requirements.\n\nThere is no container, provider registry, singleton policy, recursive construction, or automatic\nresolution. Composition roots create or spawn the dependencies they want and pass dependency clients\nexplicitly. Worker hosts may also accept dependency controllers and forward them over dependency\nchannels.\n\n```ts\nconst clock = defineWireComponent({\n  id: 'clock',\n  contract: clockContract,\n  requirements: {},\n  configSchema: z.object({}),\n  create: ({ instance, scope }) =>\n    instance({\n      scope,\n      controller: createController(clockContract, {\n        now: () => Date.now(),\n      }),\n    }),\n});\n```\n\n## In-Process Creation\n\n`component.create({ scope, dependencies, config, validate })` validates config and dependency keys,\ncreates a child scope, wires a memory transport, serves the controller, and returns a typed client.\nDisposing the instance stops the in-memory server and disposes the component scope.\n\nController factories used by components should return unvalidated controllers. The component\ncreation or worker serving boundary applies validation once.\n\n```ts\nimport { createScope } from '@emdash/shared/concurrency';\n\nconst scope = createScope({ label: 'app' });\nconst clockInstance = clock.create({\n  scope,\n  dependencies: {},\n  config: {},\n  validate: 'inputs',\n});\n\nconst counterInstance = component.create({\n  scope,\n  dependencies: {\n    clock: clockInstance.client,\n  },\n  config: {\n    initialValue: 41,\n  },\n  validate: 'inputs',\n});\n\nawait counterInstance.client.increment(undefined); // 42\nawait scope.dispose();\n```\n\nThe caller owns the parent scope. Each component creates a child scope under it. Disposing either the\ninstance or the parent scope releases the component resources.\n\n## Worker Deployment\n\n`WireWorkerHost.create(component, options)` returns a lazy supervised worker. `worker.ready()`\nstarts it and returns the stable typed client. `spawn(component, options)` eagerly starts the\nworker and returns that client.\n\nWorker component IPC uses internal framed logical channels:\n\n- `runtime` for the served component contract.\n- bootstrap messages for config and readiness.\n- one `dep:<name>` channel per contract requirement.\n\nDependency channels are forwarded only from the explicit `dependencies` object supplied by the\ncomposition root. Child workers receive typed clients for those channels; they do not locate or\nstart dependencies themselves.\n\nParent process:\n\n```ts\nimport { createScope } from '@emdash/shared/concurrency';\nimport { createWireWorkerHost } from '@emdash/wire/worker';\nimport { childProcessSpawner } from '@emdash/wire/worker/node';\nimport { clock, component as counterComponent } from './component';\nimport { workerPath } from './worker-manifest';\n\nconst scope = createScope({ label: 'app' });\nconst host = createWireWorkerHost({\n  scope,\n  processSpawner: childProcessSpawner(),\n});\n\nconst clockInstance = clock.create({\n  scope,\n  dependencies: {},\n  config: {},\n});\n\nconst counterWorker = host.create(counterComponent, {\n  executable: workerPath('counter'),\n  dependencies: {\n    clock: clockInstance.client,\n  },\n  config: {\n    initialValue: 0,\n  },\n});\n\nconst counterClient = await counterWorker.ready();\nawait counterClient.increment(undefined);\n```\n\nWorker entry:\n\n```ts\nimport { runWireComponentWorker } from '@emdash/wire/worker';\nimport { component as counterComponent } from './component';\n\nvoid runWireComponentWorker(counterComponent);\n```\n\nThe client returned by `ready()` is stable across process generations. Calls fail while the worker is\nunavailable; the client does not buffer calls during restarts.\n\n## Unrecoverable Failures\n\nComponent code can call `fatal(error)` when process-local state is poisoned and the current instance\ncannot safely serve later requests. Fatal failures retire the component scope exactly once. An\nin-process caller can observe the failure through `onFatal`; `runWireComponentWorker` exits with code\n`1`, allowing the owning `WireWorkerHost` to replace that process generation under its supervision\npolicy.\n\nUse `fatal` only when replacing the instance is the recovery mechanism, such as a native operation\nthat remains pending after its deadline and cannot be cancelled in-process. Expected request errors\nshould still be returned or thrown through the contract. A fatal report does not make an unresponsive\nnative call safe to overlap with another call in the same process; worker deployment recovers by\nterminating the process that owns the native state.\n\n## Testing Components\n\nFor in-process tests, create the component under a test scope and pass fake dependency clients\nexplicitly.\n\n```ts\nimport { createScope } from '@emdash/shared/concurrency';\nimport { createController } from '@emdash/wire/rpc';\nimport { defineWireComponent } from '@emdash/wire/worker';\nimport { z } from 'zod';\n\nconst scope = createScope({ label: 'test' });\nconst fakeClockComponent = defineWireComponent({\n  id: 'fake-clock',\n  contract: clockContract,\n  requirements: {},\n  configSchema: z.object({}),\n  create: ({ instance, scope }) =>\n    instance({\n      scope,\n      controller: createController(clockContract, {\n        now: () => 1,\n      }),\n    }),\n});\nconst fakeClock = fakeClockComponent.create({\n  scope,\n  dependencies: {},\n  config: {},\n});\n\nconst counter = component.create({\n  scope,\n  dependencies: {\n    clock: fakeClock.client,\n  },\n  config: {\n    initialValue: 0,\n  },\n  validate: 'full',\n});\n\nawait expect(counter.client.increment(undefined)).resolves.toBe(1);\nawait scope.dispose();\n```\n\nFor worker tests, use `FakeWorkerProcessSpawner` and run the child side with\n`runWireComponentWorker(component, { port: fakeProcess.childPort })`. This exercises bootstrap,\nlogical channel isolation, dependency forwarding, readiness, and reconnect behavior without starting\na real child process.\n\n## Build Metadata\n\nManual worker manifests in the desktop app and workspace-server are temporary build metadata. They\nmap thin entry files to emitted worker artifacts until a bundler plugin owns worker artifact\ngeneration. They are not part of the component runtime API.\n\n## Guidelines\n\n- Keep component factories synchronous and cheap.\n- Report poisoned process-local state through `fatal`; keep expected operation failures on the\n  contract.\n- Pass dependencies by name from the composition root; never look them up from a global registry.\n- Prefer contract requirements for shared capabilities that may cross process boundaries.\n- Use typed config for deployment-specific values such as paths, limits, executable names, and\n  feature flags.\n- Keep controller validation at the component creation or worker serving boundary, not inside every\n  controller factory.\n- Keep manual worker manifests and entry files thin; they should only select a component and call\n  `runWireComponentWorker(component)`.\n","packages/wire/docs/runtime/lifecycle.md":"# Lifecycle Utilities\n\n`Scope`, `LifecycleRegistry`, and the resource cache primitives are\ndependency-light Shared lifecycle utilities exported from\n`@emdash/shared/concurrency`. Scheduling utilities are exported separately from\n`@emdash/shared/scheduling`. Wire uses these primitives, but they do not define\nWire protocol messages.\n\n- `Scope` owns cleanup and async work for a tree of resources.\n- `LifecycleRegistry` owns explicit keyed start/stop state machines.\n- `Clock` owns sleeps, deadlines, retry backoff, and disposable timers.\n- `ResourceCache` turns keyed demand into retained resources with ref-counted leases.\n- `SharedResource` is the unkeyed form for one lazily created resource.\n- `AsyncCache` caches async values that do not have finalizers.\n- `Mailbox` is a bounded async handoff queue for one logical consumer.\n\nThey do not define wire messages and can run in browser, renderer, main process,\nor tests.\n\n## Scope\n\nUse a scope when a feature creates more than one disposable resource and those\nresources should die together:\n\n```ts\nimport { createScope } from '@emdash/shared/concurrency';\n\nconst scope = createScope({ label: 'conversation:abc', logger });\n\nscope.add(() => detachLiveState());\nscope.add(() => removeWindowListener());\nscope.use({ dispose: () => runtime.dispose() });\nscope.run('refresh', async (signal) => {\n  await clock.sleep(250, { signal });\n  await refreshVisibleState({ signal });\n});\n\nawait scope.dispose();\n```\n\nSemantics:\n\n- `state` is `open`, `closing`, or `closed`.\n- `signal` aborts synchronously when disposal begins.\n- `run(label, fn)` starts a tracked async operation with its own abort signal.\n- Cleanups run in reverse registration order.\n- Child scopes and active runs settle before the parent's own cleanups.\n- Cleanup errors are reported through `onCleanupError` and do not stop later\n  cleanups.\n- `dispose()` is idempotent.\n- `add()` on an already disposed scope runs the cleanup immediately.\n- `Run.exit` never rejects; use `Run.value()` when ordinary promise rejection is\n  desired.\n\nThat last rule makes async attach/dispose races easier to handle:\n\n```ts\nconst scope = createScope();\nawait scope.dispose();\n\nscope.add(() => detachTopic()); // runs immediately\n```\n\n## Scope Logging\n\nEvery scope has `scope.log`. If a logger is supplied at the root, children\ninherit it with a `scope` binding for the accumulated label path:\n\n```ts\nconst runtimeScope = createScope({ label: 'runtime', logger });\nconst sessionScope = runtimeScope.child('session:one');\n\nsessionScope.log.info('session attached');\n```\n\nDefault cleanup error handling logs through the scope logger:\n\n```ts\nconst scope = createScope({ label: 'view', logger });\nscope.add(() => {\n  throw new Error('cleanup failed');\n});\n\nawait scope.dispose(); // logger.warn('scope cleanup failed', ...)\n```\n\nCustom cleanup handlers receive `{ label, labelPath, logger }`:\n\n```ts\nconst scope = createScope({\n  label: 'root',\n  onCleanupError: (error, context) => {\n    context.logger.warn('custom cleanup failure', {\n      labelPath: context.labelPath,\n      error,\n    });\n  },\n});\n```\n\nUse `describeScope(scope)` to inspect the live label tree:\n\n```ts\nconsole.log(describeScope(runtimeScope));\n```\n\nThe description contains labels, label paths, lifecycle state, active runs, and\nchild descriptions. It does not expose cleanup callbacks.\n\nWhen `createScope({ clock })` is used, child scopes inherit the same clock and\nrun diagnostics use deterministic timestamps.\n\n## Scope Runs\n\nUse `run()` when async work must not outlive the scope:\n\n```ts\nconst run = sessionScope.run('attach-transcript', async (signal) => {\n  const detach = await transcript.attach(updateView, { signal });\n  sessionScope.add(detach);\n});\n\nawait run.exit;\n```\n\nDisposing `sessionScope` cancels the run and waits for it before running the\nscope's cleanups. Cancellation is cooperative: pass the provided signal to any\noperation that can wait, sleep, perform I/O, or subscribe.\n\nUse `run()` for background jobs, process transitions, replica attachment,\nrenderer bindings, and async setup. Use `add()` or `use()` for finalizers. Use\n`ResourceCache` when a keyed resource should be shared by leases.\n\nFor the full lifecycle model and invariants, see\n[Structured concurrency](./structured-concurrency.md).\n\n## LifecycleRegistry\n\nUse `LifecycleRegistry` when a feature owns a keyed set of resources that are\nstarted and stopped by explicit commands and callers need observable lifecycle\nstate:\n\n```ts\nimport { createLifecycleRegistry } from '@emdash/shared/concurrency';\nimport { ok } from '@emdash/shared';\n\nconst runtimes = createLifecycleRegistry<\n  { workspaceId: string },\n  Runtime,\n  StartRuntimeError,\n  { reason: string },\n  StopRuntimeError\n>({\n  label: 'workspace-runtimes',\n  scope: appScope,\n  keyOf: (input) => input.workspaceId,\n  start: async (input, scope, signal) => {\n    const runtime = await createRuntime(input.workspaceId, { signal });\n    scope.add(() => runtime.dispose());\n    return ok(runtime);\n  },\n  stop: async (_key, runtime, context, _scope, signal) => {\n    await runtime.stop({ reason: context?.reason, signal });\n    return ok(undefined);\n  },\n  onStateChanged: ({ key, current }) => {\n    logger.info('runtime state changed', { key, state: current.kind });\n  },\n});\n```\n\nSelection guidance:\n\n- Use `Scope` for ownership, cancellation, cleanup ordering, and async work that\n  must not outlive its feature. It is not a keyed start/stop registry.\n- Use `LifecycleRegistry` for local keyed resources with explicit `start()`,\n  `stop()`, `register()`, `state()`, and state-change observation.\n- Use `ResourceCache` when demand is lease-driven and the resource should exist\n  only while consumers hold leases or while an idle TTL retains it.\n- Use `WireWorkerHost.create(component, ...)` when the resource is a Wire worker process with\n  readiness, a stable typed client returned by `ready()`, supervision, restart backoff, and child\n  process generations.\n- Use `LiveJobSource` when callers need a Wire-visible cancellable job with progress,\n  terminal state, retention, and remote client handles.\n\n`LifecycleRegistry` differs from `Scope` by modeling resource state rather than\ngeneral cleanup. It differs from `ResourceCache` by not ref-counting leases or\ncreating resources on demand from `acquire()`. It differs from Wire workers and\n`LiveJobSource` by staying local and protocol-free: it does not supervise processes,\nserve clients, publish progress, or retain terminal job state.\n\n## Mailbox Ownership\n\nUse `Mailbox` when local producers and one consumer need an explicit bounded\nhandoff. Register it with the owning scope, then run the consumer loop under the\nsame scope:\n\n```ts\nimport { createMailbox } from '@emdash/shared/concurrency';\n\nconst mailbox = sessionScope.use(createMailbox<Event>({ capacity: 256 }));\n\nsessionScope.run('drain-events', async () => {\n  for await (const event of mailbox) {\n    await handleEvent(event);\n  }\n});\n```\n\nDisposing `sessionScope` disposes the mailbox and unblocks pending producers or\nconsumers. For state machines, overflow guarantees, and the deferred Broadcast\ncontract, see [Mailbox and Broadcast](./mailbox-and-broadcast.md).\n\n## Child Scopes\n\nUse child scopes to model ownership:\n\n```ts\nconst runtimeScope = createScope({ label: 'runtime' });\nconst sessionScope = runtimeScope.child('session:one');\n\nsessionScope.add(() => stopSession());\nruntimeScope.add(() => stopRuntime());\n\nawait runtimeScope.dispose();\n```\n\nDisposing `runtimeScope` disposes `sessionScope` first, then `stopRuntime()`.\nDisposing `sessionScope` directly deregisters it from the parent so it does not\ndispose twice later.\n\n## ResourceCache\n\n`ResourceCache` is useful when a resource should exist only while somebody is\nusing it: a live topic binding, a renderer view model, a preview server, or a\nprocess-backed session.\n\n```ts\nimport { createResourceCache } from '@emdash/shared/concurrency';\n\nconst sessions = createResourceCache({\n  scope: runtimeScope,\n  label: 'sessions',\n  key: (input: { conversationId: string }) => input.conversationId,\n  idleTtlMs: 30_000,\n  create: async ({ conversationId }, scope) => {\n    const session = await startSession(conversationId);\n    scope.add(() => session.stop());\n    return session;\n  },\n  onError: (error, key) => logger.warn('session creation failed', { key, error }),\n});\n```\n\n`create(key, scope)` is also the disposal hook. Register teardown on the supplied\nscope as soon as resources are acquired. If creation later fails, the scope is\ndisposed and partial resources are cleaned up.\n\nBehavior:\n\n- The first `acquire()` for a key calls `create()`.\n- Concurrent acquires for the same key share the in-flight creation.\n- In-flight creation is tracked as a scope run and is cancelled when the entry is\n  invalidated or the parent scope closes.\n- Each lease increments the refcount.\n- `release()` is idempotent.\n- The last release starts the grace timer.\n- Acquire during the grace timer cancels teardown and reuses the active value.\n- Failed creation is not cached; the next acquire retries.\n- `peek(key)` returns the active value if creation has completed.\n- `dispose()` force-disposes every active or retained entry.\n- Passing `scope` attaches the source and all keyed entries to a parent scope.\n  Disposing that parent scope force-disposes the source and rejects later\n  acquisitions. `label` names the source node in `describeScope()` output.\n- Every input that affects provisioning must be in `K` or fixed in the factory\n  closure. There is no acquire-time creation context.\n\nUsage:\n\n```ts\nconst lease = sessions.acquire({ conversationId: 'abc' });\nconst session = await lease.ready();\n\nawait lease.release();\nawait sessions.dispose();\n```\n\nParent scopes are useful for runtimes with several keyed resources:\n\n```ts\nconst authScope = runtimeScope.child('auth');\nconst logins = createResourceCache({\n  scope: authScope,\n  label: 'login-source',\n  key: (key: { providerId: string; methodId: string; generation: string }) =>\n    `${key.providerId}:${key.methodId}:${key.generation}`,\n  create: async (key, scope) => {\n    const login = await startLoginPty(key.providerId, key.methodId);\n    scope.add(() => login.dispose());\n    return login;\n  },\n});\n\nawait authScope.dispose(); // also disposes every active login entry\n```\n\n## Grace Windows\n\nUse `idleTtlMs` for resources whose demand can flap briefly. For example, a window\nreload may detach and reattach live model bindings within a few hundred\nmilliseconds. A short grace window avoids tearing down the underlying resource\nonly to recreate it immediately.\n\n```ts\nconst bindings = createResourceCache({\n  key: (key: { taskId: string }) => key.taskId,\n  idleTtlMs: 5_000,\n  create: async (key, scope) => {\n    const binding = client.task.transcript(key, (state) => render(state));\n    scope.add(() => binding.dispose());\n    await binding.ready;\n    return binding;\n  },\n});\n```\n\nUse `ResourceCache`, `SharedResource`, or `AsyncCache` for new shared lifetime\nwork. Avoid adding compatibility lifecycle adapters around new code.\n\nSee [../../examples/scope/client.ts](../../examples/scope/client.ts) and\n[../../examples/resource-cache/client.ts](../../examples/resource-cache/client.ts).\n","packages/wire/docs/runtime/mailbox-and-broadcast.md":"# Mailbox and Broadcast\n\n`Mailbox<T>` is a Shared bounded asynchronous handoff primitive exported from\n`@emdash/shared/concurrency`. Wire uses it when one logical consumer needs to\ndrain values from one or more producers without hand-writing arrays, waiter\nlists, terminal flags, and overflow policy.\n\n```ts\nimport { createMailbox, createScope } from '@emdash/shared/concurrency';\n\nconst scope = createScope({ label: 'download' });\nconst chunks = scope.use(createMailbox<Uint8Array>({ capacity: 64 }));\n\nconst consumer = scope.run('drain', async () => {\n  for await (const chunk of chunks) {\n    await writeChunk(chunk);\n  }\n});\n\nawait chunks.offer(new Uint8Array([1, 2, 3]));\nchunks.close();\nawait consumer.value();\n```\n\n## Mailbox\n\nA mailbox has many producers and one logical consumer. Values accepted into the\nmailbox are delivered in FIFO order. The consumer can use `take()` for one value\nat a time or `for await` for a draining loop.\n\n```ts\nconst mailbox = createMailbox<Job>({\n  capacity: 128,\n  overflow: 'suspend',\n});\n\nawait mailbox.offer(job, { signal });\nconst next = await mailbox.take({ signal });\n```\n\n### State Machine\n\n```mermaid\nstateDiagram-v2\n  [*] --> Open\n  Open --> Open: offer / take\n  Open --> Closing: close()\n  Closing --> Closed: buffer drained\n  Open --> Failed: fail(error)\n  Failed --> Failed: drain accepted values\n  Failed --> [*]: next take throws error\n  Closed --> [*]\n```\n\n`close()` is graceful: accepted values drain before consumers see the end of the\nstream. `fail(error)` is also ordered: accepted values drain first, then the next\ntake or iterator step throws the failure. `dispose()` is immediate: it clears\nbuffered values and unblocks pending producers and consumers.\n\n### Guarantees\n\n- Accepted values are delivered in FIFO order.\n- A value is delivered to at most one consumer.\n- Only one pending `take()` or active async iterator is allowed at a time.\n- Capacity is explicit and always finite.\n- Overflow is explicit: `suspend`, `reject`, `drop-oldest`, or `drop-newest`.\n- `tryOffer()` never waits.\n- `offer()` waits only when overflow is `suspend`.\n- `offer()` and `take()` can observe an `AbortSignal`; aborting one operation does\n  not close the mailbox.\n- Terminal operations are idempotent.\n- `scope.use(mailbox)` ties disposal to the owning scope, but consumer loops should\n  still run under `scope.run()` when they must not outlive the feature.\n\n### Overflow Policies\n\nUse `suspend` when every accepted value matters and producers can wait. Use\n`reject` when overflow is a protocol or invariant violation. Use `drop-oldest` for\nbest-effort queues where freshness matters more than history. Use `drop-newest`\nwhen existing queued work should be preserved and incoming work can be ignored.\n\nBlob channels use `reject` internally because dropping bytes would corrupt the\nstream. The reconnecting transport uses a shared bounded buffer with\n`drop-oldest`, preserving the existing policy that old non-blob frames are less\nvaluable than recent ones while disconnected.\n\n### Non-Goals\n\nMailbox is not:\n\n- a durable queue;\n- a multi-consumer work queue;\n- a replay log;\n- an acknowledgement or retry protocol;\n- a mutex;\n- a domain error channel.\n\nExpected domain failures should still be represented as `Result<T, E>`. Mailbox\nfailure means the local handoff itself was terminated by lifecycle, protocol, or\ntransport failure.\n\n## Broadcast Contract\n\n`Broadcast<T>` is the reserved name for a future local asynchronous fan-out\nprimitive. It should not be called `Topic<T>` because Wire already uses “topic” for\nremote live subscription addresses.\n\nThe intended shape is one mailbox per subscriber:\n\n```mermaid\nflowchart LR\n  publisher[Publisher] --> broadcast[Broadcast]\n  broadcast --> subA[\"Subscriber A Mailbox\"]\n  broadcast --> subB[\"Subscriber B Mailbox\"]\n  broadcast --> subC[\"Subscriber C Mailbox\"]\n```\n\nThe intended guarantees are:\n\n- `publish(value)` never runs subscriber application code directly.\n- Each subscriber receives values through its own mailbox.\n- Slow subscribers apply only their own overflow policy.\n- Per-subscriber delivery is FIFO for accepted values.\n- New subscribers do not receive replay.\n- Closing or failing the broadcast propagates to every subscription.\n- Publish results report subscriber count, delivered count, and dropped count.\n\nDo not implement or migrate to `Broadcast<T>` until a source has at least two\ngenuinely asynchronous consumers that need independent buffering or overflow\npolicies. Current Wire sources do not meet that bar: process stdio has one\nproduction subscriber, and observability hooks already isolate synchronous\nfailures.\n\n## Where Not To Use These\n\nDo not replace `LiveStateSource`, `LiveLogSource`, `EventStreamSource`, replicas, live\nfollowers, or reconnect attachment logic with Mailbox or Broadcast. Those\nprimitives carry generation, sequence, snapshot, resync, reconnect, retention, or\ngap semantics that Mailbox deliberately does not provide.\n\nDo not replace condition waiters such as live cursor waiters or renderer version\nwaiters. They wait for a predicate over monotonic state; they do not queue values.\n\nSee also [Structured concurrency](./structured-concurrency.md) and\n[Lifecycle utilities](./lifecycle.md).\n","packages/wire/docs/runtime/resource-cache.md":"# Resource Caches\n\n`ResourceCache`, `SharedResource`, and `AsyncCache` are Shared primitives from\n`@emdash/shared/concurrency`. Wire uses them for shared lifetimes and cached\nasync values, but they do not define Wire protocol messages.\n\n## Choose A Primitive\n\n- Use `ResourceCache<K, T>` for keyed resources that need leases and teardown:\n  process sessions, replicas, file watchers, tree resources, and connection pools.\n- Use `SharedResource<T>` for one lazily created resource with the same lease\n  semantics: a lazy worker, singleton client, or shared runtime helper.\n- Use `AsyncCache<K, T>` for data without finalizers: auth status probes,\n  metadata lookups, or other retryable reads.\n- Use `deduplicate()` middleware only when you want in-flight sharing without\n  retaining successful values.\n\n## Resource Identity\n\nEvery input that can change provisioning must be part of `K` or captured by the\nfactory closure. The cache only receives `acquire(key)`, so there is no separate\ncreation context:\n\n```ts\nconst logins = createResourceCache({\n  key: (key: { providerId: string; methodId: string; generation: string }) =>\n    `${key.providerId}:${key.methodId}:${key.generation}`,\n  create: async (key, scope) => {\n    const pty = await startLoginPty(key.providerId, key.methodId);\n    scope.add(() => pty.dispose());\n    return pty;\n  },\n});\n```\n\n## Scope Ownership\n\nEach cache owns an internal scope. Each entry gets a child scope, and creation runs\nunder `scope.run('create', ...)`. Register teardown on the entry scope as soon as\nresources are acquired. If creation fails, the entry scope is disposed and the\nnext acquire retries.\n\n`invalidate(key)` and `dispose()` force-close entries even if leases are still\nheld. Releasing a stale lease later is a no-op. This keeps owner shutdown from\nwaiting forever on leaked consumers.\n\n## Lease Behavior\n\n- Concurrent `acquire(key)` calls share one in-flight creation.\n- Every acquire receives its own idempotent lease.\n- `peek(key)` returns only completed active values.\n- Failed creation is not cached.\n- By default, releasing the final lease during creation retains the in-flight entry so a later\n  acquire can share it and the factory's terminal result remains authoritative.\n- Set `cancelPendingOnRelease: true` for resources such as watchers that have no useful lifetime\n  without a lease. Pending work must then observe the entry scope's signal and clean up late results.\n- `idleTtlMs` retains a zero-ref entry briefly so flapping demand can reuse it.\n- Acquiring during teardown waits for teardown to finish, then provisions fresh.\n\n## AsyncCache\n\n`AsyncCache` shares concurrent loads and caches only successes:\n\n```ts\nconst statuses = createAsyncCache({\n  key: (providerId: string) => providerId,\n  ttlMs: 15 * 60 * 1000,\n  load: (providerId, signal) => checkAuthStatus(providerId, { signal }),\n});\n```\n\n`get()` uses a cached value when fresh, `refresh()` cancels the current load and\nstarts a new generation, `set()` publishes an explicit value, and failures are\nnever retained.\n","packages/wire/docs/runtime/scheduling.md":"# Scheduling\n\n`@emdash/shared/scheduling` centralizes time, retry, and\ncancellation-sensitive waits. Wire uses these Shared primitives, and runtime\ncode should depend on `Clock` instead of calling platform timer APIs directly.\n\n```ts\nimport { retry, retrySchedules, systemClock } from '@emdash/shared/scheduling';\n\nawait retry(\n  () => transport.connect(),\n  {\n    clock: systemClock,\n    schedule: retrySchedules.exponential({ initialMs: 100, maxMs: 2_000 }),\n    shouldRetry: (error) => isTransient(error),\n  }\n);\n```\n\n## Clock\n\n`Clock` has three operations:\n\n- `now()` returns wall-clock milliseconds for diagnostics and deadlines.\n- `schedule(delayMs, callback)` returns a disposable `TimerHandle`.\n- `sleep(delayMs, { signal })` returns an abortable promise.\n\nUse `Clock` for sleeps, backoff, debounce, retention, idle TTLs, readiness\ndeadlines, and test polling. Keep direct `Date.now()` or `performance.now()` for\nprotocol timestamps, generated IDs, and duration measurement where the wall clock\nor monotonic timer is the data being recorded.\n\n## TimerHandle Ownership\n\nEvery scheduled callback must have one owner. Register the handle with a `Scope`,\nstore it on the owning entry, or dispose it in all terminal paths:\n\n```ts\nconst handle = clock.schedule(5_000, () => expire(id), { unref: true });\nscope.add(() => handle.dispose());\n```\n\n`TimerHandle.dispose()` is idempotent. The `active` flag becomes false before the\ncallback runs, so callback and disposal races remain at-most-once.\n\n## Timeouts\n\n`runWithTimeout(work, { timeoutMs, signal, clock })` derives a child\n`AbortSignal`, schedules a deadline on the provided `Clock`, and disposes its\ntimer in every terminal path. The returned promise rejects with `TimeoutError`\nwhen the deadline expires, but parent cancellation still rejects with the\nparent's abort reason.\n\n```ts\nconst result = await runWithTimeout(\n  (signal) => loadWorkspace({ signal }),\n  { timeoutMs: 5_000, signal: parentSignal, clock }\n);\n```\n\nThe timeout rejects promptly even when work ignores the derived signal, but the\nunderlying JavaScript work can only stop cooperatively. Pass the derived signal\ninto sleeps, process waits, transports, and any helper that can observe\ncancellation.\n\n## ManualClock\n\nTests should import `ManualClock` from `@emdash/shared/testing`. It preserves FIFO\nordering for timers with the same deadline and flushes resumed promise\nmicrotasks while advancing:\n\n```ts\nconst clock = new ManualClock(1_000);\nconst done = clock.sleep(250);\n\nawait clock.advanceBy(250);\nawait done;\n```\n\nUse `runAll({ maxTimers })` only when the test expects the scheduled work to\ndrain. The guard prevents accidental infinite timer loops.\n\n## Retry Semantics\n\n`RetrySchedule.delayFor(0)` is the delay before the first retry. Attempt `0` is\nthe initial operation call, attempt `1` is the first retry, and so on.\n`undefined` means retries are exhausted.\n\nSchedules are stateless and composable: fixed, finite sequence, repeat-last\nsequence, exponential, limited, and deterministic jitter. Inject a deterministic\nrandom function into jittered schedules in tests.\n\n`retry()` is exception-based. Domain errors that are expected API results should\nremain `Result<T, E>` values; use `shouldRetry` to classify thrown transport or\nruntime failures.\n\n## Wire Retry Vocabulary\n\nWire expresses all retry timing through one schedule vocabulary:\n`RetrySchedule` and the `retrySchedule()` constructor from\n`@emdash/shared/scheduling`. The reconnecting transport's reconnect delays,\nworker-slot supervision, the follower's `resyncRetry` policy, and per-call\nopt-in retries all consume it. Shared vocabulary, not shared state — each site\nkeeps its own independently tunable schedule:\n\n```ts\nimport { retrySchedule } from '@emdash/shared/scheduling';\n\nconst supervision = {\n  restart: 'on-failure',\n  schedule: retrySchedule({ delaysMs: [250, 1_000, 2_500], repeatLast: true, maxRetries: 5 }),\n};\n```\n\n## Periodic Work\n\nPrefer abortable loops or chained one-shot timers over overlapping intervals.\nThat keeps disposal ordered and avoids installing work after an owner has begun\nclosing:\n\n```ts\nscope.run('poll', async (signal) => {\n  while (!signal.aborted) {\n    await refresh({ signal });\n    await clock.sleep(1_000, { signal });\n  }\n});\n```\n","packages/wire/docs/runtime/structured-concurrency.md":"# Structured Concurrency\n\nShared `Scope.run()` from `@emdash/shared/concurrency` tracks asynchronous work\nunder the same owner as subscriptions, processes, timers, and other resources.\nWire uses it when work must not outlive the feature that started it.\n\n```ts\nconst scope = createScope({ label: 'session:abc', logger, clock });\n\nconst run = scope.run('load-history', async (signal) => {\n  const history = await loadHistory({ signal });\n  return history;\n});\n\nconst history = await run.value();\nawait scope.dispose();\n```\n\n## Concepts\n\n- `Scope` owns a lifecycle tree. Parent disposal closes children.\n- `Run<A>` is one async operation owned by a scope.\n- `Run.exit` never rejects. It resolves to `success`, `failure`, or `cancelled`.\n- `Run.value()` converts the exit back to normal promise semantics.\n- `Lease<A>` is temporary use of a shared resource, usually from `ResourceCache`\n  or `SharedResource`.\n\n```mermaid\nflowchart TD\n  featureScope[\"Feature Scope\"] --> childScope[\"Child Scope\"]\n  featureScope --> runA[\"Run: load-history\"]\n  childScope --> runB[\"Run: attach-topic\"]\n  childScope --> cleanup[\"Finalizers\"]\n```\n\n## Lifecycle\n\nDisposal is idempotent and ordered:\n\n1. The scope moves from `open` to `closing`.\n2. `scope.signal` aborts synchronously.\n3. Existing child scopes begin closing.\n4. Active runs are cancelled.\n5. The scope waits for child scopes and runs.\n6. Cleanups run in reverse registration order.\n7. Cleanup errors are logged and later cleanups still run.\n8. The scope moves to `closed`.\n\nStarting a run after closing has begun does not invoke the operation. The returned\nrun is already cancelled. Adding a cleanup after a scope is closed runs it\nimmediately as best-effort cleanup.\n\n## Invariants\n\n- Every child scope has one parent.\n- Every run belongs to one scope.\n- `dispose()` returns the same promise for every caller.\n- `scope.signal` aborts before the first `await` in disposal.\n- Parent cancellation propagates to children.\n- Sibling runs do not cancel each other.\n- `Run.exit` never rejects.\n- A cancelled run is reported as `cancelled` even if the operation later returns.\n- A failed run is logged through the scope logger.\n- A scope is not closed until owned runs and children have settled.\n\nJavaScript cancellation is cooperative. A run that ignores its signal can keep\n`dispose()` pending. Pass the run signal into network calls, sleeps, process\nwaits, and any helper that can observe cancellation.\n\n`Scope` accepts an optional `Clock`. Child scopes inherit it, and active-run\ndiagnostics use `clock.now()` for deterministic timestamps in tests. Prefer\n`clock.sleep(ms, { signal })` for run-owned delays so disposal can interrupt the\nwait immediately.\n\nA run must not await disposal of its own owning scope. That creates a cycle:\n`dispose()` waits for the run, while the run waits for `dispose()`.\n\n## When To Use It\n\nUse `run()` for:\n\n- background jobs and progress sources;\n- pending replica attachment or snapshot work;\n- process spawn, restart backoff, and exit handling;\n- renderer bindings with fire-and-forget RPCs;\n- async setup that must be cleaned up if later setup fails.\n\nUse `add()` or `use()` for:\n\n- subscriptions;\n- timers and observers;\n- resource finalizers;\n- objects with `dispose()`.\n\nUse child scopes for:\n\n- per-session resources under a runtime;\n- per-job resources under a job source;\n- per-consumer resources under a shared cache;\n- modal or view-local lifetimes.\n\nUse `ResourceCache` for keyed, ref-counted resources. Its creation runs under\nthe entry scope, so invalidating an entry or disposing the parent scope cancels\nand joins in-flight creation before finalizers run.\n\nUse `Mailbox` for bounded asynchronous handoff between producers and one logical\nconsumer. The mailbox owns buffering and terminal-state wakeups; run the consumer\nloop under `scope.run()` when it must not outlive the feature:\n\n```ts\nconst mailbox = scope.use(createMailbox<Message>({ capacity: 128 }));\n\nscope.run('drain messages', async () => {\n  for await (const message of mailbox) {\n    await handleMessage(message);\n  }\n});\n```\n\n`Broadcast<T>` is documented as a future local fan-out primitive for multiple\nasynchronous consumers with independent backpressure. Do not use Mailbox or\nBroadcast to replace live-state snapshots, retained logs, event streams, replica\ncursors, or reconnect attachment logic; those protocols carry generation,\nsequence, retention, or resync semantics.\n\nDo not use `run()` as:\n\n- a mutex or serialization primitive;\n- a retry policy;\n- a cache;\n- a domain error channel.\n\nExpected domain failures should still be returned as `Result<T, E>`. A run\nfailure means the operation itself threw unexpectedly.\n\n## Diagnostics\n\n`describeScope(scope)` includes the lifecycle state, active child scopes, and\nactive runs:\n\n```ts\nconsole.log(describeScope(scope));\n```\n\nEach run description includes its label, start time, and whether cancellation has\nstarted. This is useful for finding retained sessions, stuck process transitions,\nor long-running renderer bindings during tests and debugging.\n","packages/wire/docs/runtime/workers.md":"# Workers\n\nA worker is a named, process-hosted `WireComponent` owned by a `WireWorkerHost`.\nThe host creates logical workers and returns a stable typed client from readiness. Internally, each worker has one\nslot that owns readiness, supervision, reconnects, graceful shutdown, and process generations.\nLow-level spawners create exactly one process generation.\n\nSee [Components](./components.md) for the authored component shape, local creation examples,\ndependency requirements, and testing guidance. This page focuses on process supervision.\n\n## Layers\n\n```mermaid\nflowchart LR\n  App[\"Application composition\"] --> Host[\"WireWorkerHost\"]\n  Host --> Slot[\"Internal worker slot\"]\n  Slot --> Link[\"Internal worker link\"]\n  Slot --> Spawner[\"WorkerProcessSpawner\"]\n  Spawner --> Process[\"WorkerProcess\"]\n  Process --> Runner[\"runWireComponentWorker\"]\n  Runner --> Controller[\"Component Controller\"]\n  Host --> Deps[\"Explicit dependency channels\"]\n```\n\n- `WireWorkerHost` is a process ownership boundary. Composition roots inject a\n  `WorkerProcessSpawner`, logger, clock, and defaults.\n- The internal worker slot is the only multi-generation state machine. It keeps one stable\n  connection/client while child processes restart underneath it; it is not an author-facing API.\n- `WorkerProcessSpawner` is platform-specific and creates one generation. Use\n  `childProcessSpawner()` from `@emdash/wire/worker/node` for Node child\n  processes.\n- `runWireComponentWorker(component)` is the child-side IPC bridge. It bootstraps typed config,\n  connects explicit dependency clients, creates the component, serves its controller, and signals\n  readiness.\n\n## Parent Side\n\n```ts\nimport { createWireWorkerHost } from '@emdash/wire/worker';\nimport { childProcessSpawner } from '@emdash/wire/worker/node';\nimport { createScope } from '@emdash/shared/concurrency';\nimport { counterComponent } from './component';\nimport { workerPath } from './worker-manifest';\n\nconst scope = createScope({ label: 'main' });\nconst host = createWireWorkerHost({\n  scope,\n  processSpawner: childProcessSpawner(),\n});\n\nconst worker = host.create(counterComponent, {\n  name: 'counter',\n  executable: workerPath('counter'),\n  dependencies: {},\n  config: {},\n});\n\nconst client = await worker.ready();\nawait client.increment(undefined);\nawait host.dispose();\n```\n\n`worker.ready()` starts the process, waits for the child ready signal, and returns the stable\ntyped client. The pending worker intentionally exposes no public client. Calls through a previously\nreturned client fail fast while the worker is unavailable; Wire does not buffer requests during\ndowntime.\n\nStartup and exposure are explicit composition-site decisions. For eager startup, await\n`worker.ready()` under an owning scope or call `host.spawn(component, options)` to get the client. For Electron\nwindows, compose a forwarding controller and use `exposeWireToWindows()` with a `beforeOpen` hook\nthat awaits `worker.ready()`.\n\n## Child Side\n\n```ts\nimport { runWireComponentWorker } from '@emdash/wire/worker';\nimport { counterComponent } from './component';\n\nvoid runWireComponentWorker(counterComponent);\n```\n\nThe child helper resolves the parent IPC channel, requests bootstrap config and dependency channels,\nserves framed runtime wire messages, sends the ready signal after the component controller is\ninstalled, disposes the child scope on shutdown/disconnect, and exits with code `1` if creation\nfails. It also exits with code `1` when the component reports an unrecoverable failure through\n`fatal(error)`. The worker slot treats that exit like any other failed generation: it disconnects the\nold transport and applies the configured supervision schedule before starting a fresh process.\n","packages/wire/examples/README.md":"# @emdash/wire Examples\n\nThese examples show the public surface of `@emdash/wire`. Every example imports\nthe package entrypoints only (`/rpc`, `/live`, `/state`, via their in-repo\nsource paths) and splits the authoritative server side from the client-side\nbinding so the boundary looks like a real transport without adding an adapter.\n\n## Documentation\n\nFor conceptual docs that explain how the examples fit together, see\n[../docs/README.md](../docs/README.md).\n\nRun them from the repository root:\n\n```bash\npnpm --filter @emdash/wire run example:state-kernel\npnpm --filter @emdash/wire run example:live-log\npnpm --filter @emdash/wire run example:event-stream\npnpm --filter @emdash/wire run example:mailbox\npnpm --filter @emdash/wire run example:cancellation\npnpm --filter @emdash/wire run example:api-definition\npnpm --filter @emdash/wire run example:dedupe\npnpm --filter @emdash/wire run example:job-contract\n```\n\nExamples:\n\n- `state-kernel/` demonstrates the state kernel end to end: `cell` + `family`\n  + `expose` on the server, `remote()` + `observe` + `optimistic` on the\n  client, including an optimistic mutation settling against the authoritative\n  cursor.\n- `live-log/` demonstrates a contract-level `liveLog()` endpoint served by a\n  `LiveLogSource`, consumed through `createLiveLogReplicaCache` and\n  `ReplicaLog`.\n- `event-stream/` demonstrates a contract-level `eventStream()` endpoint served\n  by `createEventStreamHost` with client-side subscribe and gap handling.\n- `mailbox/` demonstrates bounded local producer/consumer handoff, graceful close,\n  and explicit overflow.\n- `cancellation/` demonstrates procedure cancellation with `AbortSignal` and\n  server-side abort on disconnect.\n- `api-definition/` isolates contract definition with `defineContract`,\n  `procedure`, `liveModel`, `liveLog`, and live model contract\n  mutations.\n- `dedupe/` demonstrates server-side `deduplicate()` middleware from\n  `@emdash/shared/requests` for in-flight procedure calls.\n- `job-contract/` demonstrates the contract-level `liveJob()` endpoint with start,\n  progress, cancellation, terminal result, and reattach.\n- `scope/` and `resource-cache/` demonstrate the `@emdash/shared/concurrency`\n  primitives the live layer builds on.\n","packages/wire/src/state/README.md":"# `@emdash/wire` reactive state (`wire/src/state/`)\n\n> **Status: implemented in parallel to `wire/src/live/`.** The primitives in\n> this folder are available as `@emdash/wire/state` and are in production use:\n> desktop feature slices (tasks, workspaces, source-control, editor, projects)\n> author providers with `expose` and consume them with `remote`. See\n> [05-migration.md](./05-migration.md) for the remaining migration plan.\n\nThis folder holds the reactive state kernel for wire: a small set of\nsignals-style primitives that unify how authoritative state is produced,\nderived, and published over the wire — and how replicas of it are consumed on\nthe other side.\n\nIt replaces the *value-reactivity* portion of `wire/src/live/` (the four ad-hoc\n\"a value that changes\" flavors: `LiveStateSource` used directly by features,\n`ComputedLiveState`, deleted `BatchedLiveState`, `bindMachineToLiveState`) and the\nhand-written provider/lease boilerplate around them. It does **not** replace\nthe wire protocol (`live/protocol/`), the replica transport, live logs, event\nstreams, or live jobs — those stay. See\n[04-wire-integration.md](./04-wire-integration.md) for the exact boundary.\n\n## Why\n\nToday every stateful runtime hand-rolls the same machinery: dirty flags,\ndebounce timers, one-in-flight-plus-one-queued refresh collapse, lease\nlifecycles, imperative invalidation fan-out, and per-provider\n`acquireState`/`ready`/`release` plumbing. The former file `TreeResource` spent\n~200 lines on scheduling; `RepositoryResource` manually fans invalidations out to every\ncheckout; every `LeasedLiveModelProvider` implementation repeats the same ~40\nlines. The kernel moves all of that into one tested core and leaves feature\ncode with declarations:\n\n```ts\n// A keyed, DB-backed live query — the whole provider.\nconst taskList = family((key: { projectId: string }, scope) =>\n  query({\n    fetch: () => selectTaskListView(db, key),\n    pokes: [tasksChannel.match(key), conversationsChannel.match(key)],\n    scope,\n  }));\n\nexpose(tasksContract.taskList, taskList);\n```\n\n## The primitives at a glance\n\n| Primitive | One-liner | Doc |\n|---|---|---|\n| `cell` | Writable value; the leaf for in-memory sources | [02](./02-primitives.md#cell) |\n| `derived` | Pure, auto-tracked computation over other reactives | [02](./02-primitives.md#derived) |\n| `query` | Async computed from an *external* source (DB, git, fs); declared invalidation via pokes; write-through via `settle` | [02](./02-primitives.md#query) |\n| `family` | Keyed instantiation of any reactive, refcounted with linger GC | [02](./02-primitives.md#family) |\n| `observe` | The only side-effect edge: subscribe a callback, owned by a `Scope` | [02](./02-primitives.md#observe) |\n| `peek` | Untracked read | [02](./02-primitives.md#peek-and-untracked-reads) |\n| `batch` | Group synchronous writes into one turn with shared metadata | [02](./02-primitives.md#batch) |\n| `pokeChannel` | Named invalidation channel connecting external change signals to queries | [02](./02-primitives.md#pokechannel) |\n| `optimistic` | Pending-edit overlay over an authoritative base: apply-now, ack-prune, rollback-for-free | [02](./02-primitives.md#optimistic) |\n| `pin` / `prefetch` | Explicit demand at lifecycle boundaries: fetch at mount/intent, not at render | [02](./02-primitives.md#pin-and-prefetch) |\n| `expose` | Publish a reactive/family as a wire live model (generates the provider) | [04](./04-wire-integration.md#expose) |\n| `remote` | Consume a wire live model as a reactive family (wraps the replica) | [04](./04-wire-integration.md#remote) |\n\nOwnership and disposal always flow through `Scope` from\n`@emdash/shared/concurrency` — there is no manual `release()` in the common\npath. On the client side, demand is driven by `pin`s owned by lifecycle\nboundaries (view scopes), never by render-time observability — see\n[01-concepts.md §6](./01-concepts.md#demand-is-a-policy-not-a-rendering-accident).\n\n## Testing\n\nThe state module is tested in four deterministic tiers:\n\n- Kernel tests in `core/kernel.test.ts` and `core/family.test.ts` cover turns,\n  dynamic dependencies, error recovery, observer isolation, mutation-id\n  folding, and linger GC.\n- Primitive tests in `query.test.ts`, `optimistic.test.ts`, and `pin.test.ts`\n  cover async races, write-through, TTL rollback, generation pruning, and\n  demand aggregation.\n- Bridge tests in `bridge/expose.test.ts` cover readiness, leases, and\n  derived-hop mutation observation.\n- `bridge/roundtrip.test.ts` runs `expose -> wire transport -> remote ->\n  optimistic` over the real test wire harness to protect read-your-writes and\n  no-flicker behavior end to end.\n\n## Reading order\n\n1. [01-concepts.md](./01-concepts.md) — the mental model: nodes, turns,\n   observation, statuses, the tracked/declared boundary, publish metadata.\n2. [02-primitives.md](./02-primitives.md) — reference for each primitive:\n   signatures, semantics, options, edge cases.\n3. [03-composition.md](./03-composition.md) — how primitives compose; worked\n   examples (git runtime, file tree, DB-backed lists, optimistic overlays);\n   the granularity rule that keeps performance flat.\n4. [04-wire-integration.md](./04-wire-integration.md) — `expose`/`remote`,\n   mutations, read-your-writes cursors, and the exact mapping onto the\n   existing `live/protocol/` machinery.\n5. [05-migration.md](./05-migration.md) — legacy class → primitive mapping,\n   what gets deleted, and the incremental migration order.\n\n## Design lineage\n\nThe API descends from SolidJS (ownership tree, `createResource`'s async\nstates, `untrack`, lazy equality-gated memos, `from()` interop), Jotai/Recoil\n(`atomFamily` → `family`), and TanStack Query (`setQueryData` vs\n`invalidateQueries` → `settle` vs pokes). The one deliberate departure from\nSolid: dependency tracking is automatic only where reads are interceptable\n(`derived` over in-process reactives); at the external-source boundary\n(`query` over DB/fs/git), dependencies are *declared* as pokes so that \"poke\ncoverage = query inputs\" stays a greppable, auditable property.\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# Project Overview\n\nEmdash is a local-first, cross-platform Electron app for running multiple AI coding\nagents in parallel. Each task is isolated in its own Git worktree and can run locally\nor over SSH, while the desktop app coordinates provider CLIs, ACP chat sessions,\nterminal sessions, issue and PR integrations, diff review, and release packaging.\n\n## Repository Structure\n\nThis is a pnpm workspace monorepo. The Electron app lives in\n`apps/emdash-desktop/` as `@emdash/emdash-desktop`; shared packages live under\n`packages/`. Unless a path is prefixed with `packages/` or another app, app paths\nsuch as `src/...`, `drizzle/`, `scripts/`, and `build/` are relative to\n`apps/emdash-desktop/`.\n\nRepo root:\n\n- `.claude/` - Local Claude agent settings for this checkout.\n- `.github/` - GitHub issue templates, reusable actions, CI, and release workflows.\n- `agents/` - Agent-facing architecture, workflow, convention, integration, and risk docs.\n- `apps/emdash-desktop/` - The Electron desktop app.\n- `apps/workspace-server/` - Remote workspace server and its Docker-based dev stack.\n- `packages/chat-ui/` - Shared transcript and ACP chat renderer with Storybook coverage.\n- `packages/core/` - Transport-agnostic runtime primitives, including ACP session logic.\n- `packages/plugins/` - Agent provider plugin definitions, hooks, and ACP adapters.\n- `packages/shared/` - Shared primitives such as result types, logging, and markdown helpers.\n- `packages/theme/` - Theme token codegen that emits the generated theme CSS.\n- `packages/ui/` - Shared React UI components, theme tokens, recipes, and primitives.\n- `packages/wire/` - Typed wire protocol contracts shared by app and workspace server.\n- `pnpm-workspace.yaml` - Workspace package globs for `apps/*` and `packages/**`.\n- Root config files - `package.json`, `nx.json`, `.nvmrc`, `.oxfmtrc.json`,\n  `.oxlintrc.json`, and lockfile/configuration owned at the workspace root.\n\nInside `apps/emdash-desktop/`:\n\n- `build/` - Electron packaging assets; avoid edits unless working on packaging/signing.\n- `drizzle/` - Generated Drizzle SQL migrations and metadata.\n- `scripts/` - Release, verification, and build support scripts.\n- `src/main/` - Electron main process: bootstrap, Wire gateway, DB, and host services.\n- `src/entry/` - Electron entry points: `main.ts` (main process) and `preload.ts`, the typed\n  preload bridge exposed to the renderer.\n- `src/renderer/` - React composition shell, shared browser infrastructure, and tests.\n- `src/core/` - Vertical slices with portable APIs, Node implementations, browser UI, and manifests.\n- `src/types/` - Ambient and cross-cutting TypeScript declarations.\n- `tooling/` - App-level development and test infrastructure not bundled into production.\n\n## Build & Development Commands\n\nThe only prerequisite is any `pnpm` on PATH: the root `package.json` pins\n`packageManager: pnpm@10.28.2` and `devEngines.runtime` node `24.14.0` with\n`onFail: \"download\"`, so pnpm provisions the pinned toolchain itself (no nvm or\ncorepack). The committed `mise.toml` is an optional convenience for mise users.\nRoot scripts are powered by Nx and run package targets in dependency order with\nlocal caching where configured.\n\nGolden-path commands (see [Developer flows](agents/workflows/flows.md) for the\nfull index — one blessed command per flow, database flows, Storybook,\npackaging, remote development, and escape hatches):\n\n```bash\npnpm install        # complete setup from the repo root — nothing else needed\npnpm run doctor     # report-only environment health check\npnpm run dev        # full workspace dev (root); app-only from apps/emdash-desktop/\npnpm run build      # build all workspace projects\npnpm run check      # full merge gate: format, lint, typecheck, test\n```\n\nThe four gate commands also run individually from the repo root:\n\n```bash\npnpm run format\npnpm run lint\npnpm run typecheck\npnpm run test\n```\n\nUse an isolated development database for schema or migration work:\n\n```bash\nEMDASH_DB_FILE=/tmp/emdash-scratch.db pnpm run dev\n```\n\nRun focused database validation from `apps/emdash-desktop/`:\n\n```bash\npnpm run db:fixtures\npnpm run test:migrations\n```\n\nDeploy releases only when explicitly asked to do release work:\n\n```bash\ngh workflow run release-prod.yml --ref main -f arch=both\ngh workflow run release-canary.yml --ref main -f arch=both\ngh workflow run release-workspace-server.yml --ref main -f channel=stable\n```\n\nProduction releases publish artifacts to GitHub Releases and Cloudflare R2. Canary\nreleases currently publish to R2 only. See\n[workspace-server packaging](apps/workspace-server/docs/packaging.md) for its independent release\nand channel rules.\n\n## Code Style & Conventions\n\n- Use `pnpm`; do not introduce npm or yarn lockfile churn.\n- Format with `oxfmt`; config is `.oxfmtrc.json`.\n- Keep formatted lines near the configured `printWidth` of 100 characters.\n- Use 2 spaces, semicolons, single quotes in TS, double quotes in JSX, LF endings,\n  trailing commas where valid in ES5, and sorted imports.\n- Lint with `oxlint`; config is `.oxlintrc.json` with correctness, TypeScript, React\n  hooks, and local repo rules enabled.\n- TypeScript strict mode is enabled; app targets share `apps/emdash-desktop/tsconfig.json`.\n- Avoid `any`; if a registry or boundary needs it, keep the escape local and documented.\n- Use top-level `import` statements; do not use `require()`.\n- Never re-export as a shortcut; import from the original source.\n- Components use `PascalCase`; hooks use `useX` camelCase or an existing local pattern.\n- Tests use `*.test.ts` or `*.test.tsx`.\n- Slice Wire controllers live in `src/core/features/<feature>/node/` and are aggregated by\n  `src/core/manifests/node/controllers.ts`; they delegate to imported operation or service\n  functions.\n- Renderer-main calls go through Wire via the seeded connection seam in\n  `src/core/primitives/wire/browser/connection.ts`: the renderer bootstrap seeds it once at\n  startup, and each slice exposes a typed domain client from its `api/` (for example\n  `getMcpClient()`) built on `domainClient`.\n- Feature UI lives under `src/core/features/<feature>/browser/`; the editor slice owns Monaco and\n  file rendering under `src/core/features/editor/browser/`; portable browser primitives (modals,\n  views, navigation, commands, scoped stores) live under `src/core/primitives/`; the thin\n  renderer host shell lives under `src/renderer/lib/`.\n- New feature modals and views must be exposed through the owning slice's `contributions/browser.ts`\n  and aggregated by `src/core/manifests/browser/browser-contributions.ts`.\n- New task tabs contribute providers through `src/core/features/<feature>/contributions/tabs.ts`,\n  aggregated by `src/core/manifests/browser/task-tab-contributions.ts`; the workbench registry\n  lives at `src/core/features/workbench/api/browser/task-tab-registry.ts`.\n- New commands are defined with `defineCommand` (`src/core/primitives/commands/`) in the owning\n  slice's `contributions/commands.ts`, aggregated by\n  `src/core/manifests/shared/command-catalog.ts`; view scopes\n  (`src/core/primitives/view-scopes/`) bind availability and implementation.\n- Commit messages should follow Conventional Commits:\n\n```text\n<type>(<scope>): <short imperative summary>\n\nExamples:\nfix(opencode): change initialPromptFlag from -p to --prompt for TUI\nfeat(docs): add changelog tab with GitHub releases integration\n```\n\n## Architecture Notes\n\n```mermaid\nflowchart LR\n  User[User] --> Renderer[React renderer]\n  Renderer --> Clients[Typed Wire domain clients]\n  Clients --> Seam[Seeded wire connection seam]\n  Seam --> Preload[Preload wire port]\n  Preload --> Main[Electron main process]\n  Main --> Gateway[Desktop Wire gateway]\n  Gateway --> Controllers[Slice Wire controllers]\n  Controllers --> Services[Domain services]\n  Services --> DB[(SQLite via Drizzle)]\n  Services --> Runtime[Runtime services]\n  Runtime --> PTY[PTY sessions]\n  Runtime --> ACP[ACP sessions]\n  Runtime --> SSH[SSH and SFTP]\n  Services --> VCS[Git, GitHub, GitLab, PRs]\n  Services --> Issues[Issue integrations]\n  Services --> MCP[MCP and skills]\n  ACP --> CoreAcp[@emdash/core ACP runtime]\n  ACP --> Plugins[@emdash/plugins providers]\n  Renderer --> ChatUI[@emdash/chat-ui]\n  Main --> Events[Live models and typed events]\n  Events --> Renderer\n```\n\nThe app boots from `src/entry/main.ts`, which runs the phased bootstrap in\n`src/main/bootstrap/` (environment, database, window creation, recovery). Renderer-main\ntraffic is served by the desktop Wire gateway (`src/main/gateway/desktop-wire.ts`) from the\ncontract assembled in `src/core/manifests/shared/desktop-wire-contract.ts` and the controllers\naggregated in `src/core/manifests/node/controllers.ts`. The preload bridge\n(`src/entry/preload.ts`) exposes only `requestWirePort` and `getPathForFile`. The renderer is\na React app that calls typed Wire domain clients, subscribes to live models and typed events,\nand coordinates views, tabs, modals, commands, project state, terminal state, and task\nworkflows.\n\nTask execution has two runtime paths. Legacy/TUI conversations run through PTY\nservices under `packages/core/src/services/pty/` and the terminal/TUI runtimes under\n`packages/core/src/runtimes/terminals/` and `packages/core/src/runtimes/tui-agents/`.\nStructured chat conversations use ACP: provider plugins in `packages/plugins/` expose\nACP behavior, `packages/core/src/runtimes/acp/` owns protocol/session state, terminal\nmanagement, and process hosting, the desktop launches that runtime as a Wire component worker\nfrom `src/main/gateway/entries/acp.ts` (`src/main/core/acp/` bridges agent status), and\n`src/core/features/conversations/browser/acp/` maps updates into `@emdash/chat-ui`.\n\nMain-process adapter domains live under `src/main/core/`: ACP, agent status, app,\ndependencies, file search, files, Git, preview servers, provider accounts, runtime,\nshared, terminal shell, and utils. Portable domain logic lives in vertical slices\nunder `src/core/` and in `packages/core/` (for example PTY services under\n`packages/core/src/services/pty/` and resource monitoring under\n`packages/core/src/runtimes/resource-usage/`). Expected failures should use the\n`Result<T, E>` pattern from `@emdash/shared` or the app-local result helpers.\n\n## Testing Strategy\n\nLocal merge gate:\n\n```bash\npnpm run format\npnpm run lint\npnpm run typecheck\npnpm run test\n```\n\n- Root `pnpm run test` uses Nx to run every workspace package test target.\n- App tests run with Vitest projects in `apps/emdash-desktop/vitest.config.ts`.\n- App `node` tests cover `src/**/*.test.ts` except DB, migration, and browser tests.\n- App `main-db` tests cover main-process integration tests that need real SQLite.\n- App `fixtures` tests generate DB fixtures via `pnpm run db:fixtures`.\n- App `migrations` tests validate Drizzle migrations via `pnpm run test:migrations`.\n- App `scripts` tests cover release and support scripts under `scripts/**/*.test.ts`.\n- App `browser` tests use Playwright-backed `@vitest/browser` for renderer behavior.\n- `packages/core` has ACP, dependency, plugin helper, Git, FS, and runtime unit tests.\n- `packages/chat-ui` has node, browser, perf, and benchmark test targets.\n- `packages/ui`, `packages/shared`, and `packages/plugins` run their package-local tests.\n- Integration-style tests create temporary repos and worktrees in `os.tmpdir()`.\n- CI runs `.github/workflows/code-consistency-check.yml` with `nx affected` for\n  `format:check`, `typecheck`, `lint`, and `test` on touched projects and\n  dependents; the Playwright-backed `browser` Vitest projects are skipped there.\n- Tests are still expected locally before merge even where CI coverage is narrower.\n\n## Security & Compliance\n\n- The project is licensed under Apache-2.0; see `LICENSE.md`.\n- Do not commit secrets, tokens, private keys, app databases, logs, build artifacts,\n  generated dependency folders, or release artifacts.\n- Application secrets are stored through encrypted app secret services and Electron\n  safe storage; SSH credentials are managed through SSH services and OS-backed storage.\n- Release secrets live in GitHub Actions secrets, including PostHog, Cloudflare R2,\n  Apple signing/notarization, Azure Trusted Signing, and Cachix credentials.\n- Telemetry must remain optional; users can disable it with `TELEMETRY_ENABLED=false`\n  or in app settings.\n- File logging must preserve redaction of common secret patterns.\n- PTY environment construction must go through `packages/core/src/services/pty/api/terminal-env.ts`.\n- Treat ACP process spawning, SSH command construction, shell escaping, PTY spawning,\n  and worktree paths as security-sensitive.\n- Do not bypass path-safety, shell escaping, or validation helpers.\n- Use `pnpm-lock.yaml` for dependency integrity and review dependency changes.\n- This checkout does not define repo-local Dependabot config, CODEOWNERS, or SECURITY.md;\n  do not assume extra repository-owned security automation beyond the workflows present\n  in `.github/workflows/`.\n- Dependency changes must keep `pnpm-lock.yaml` in sync, preserve `packageManager` and\n  `pnpm.onlyBuiltDependencies`, and avoid introducing new install scripts or native\n  builds without explicit review.\n- CI installs with `pnpm install --frozen-lockfile --ignore-scripts` in\n  `.github/workflows/code-consistency-check.yml`; changes that rely on install-time\n  side effects need clear justification.\n- Prefer existing dependencies and workspace packages over adding new third-party\n  packages. When adding a dependency, document why the existing stack is insufficient\n  and check license/security posture before committing the lockfile change.\n\n## Agent Guardrails\n\n- Start with this file for repo-wide context and required commands.\n- Load only the relevant `agents/` topic page for the area you are changing.\n- Prefer updating the smallest applicable `agents/` page over expanding this file.\n- If nested `AGENTS.md` files are added later, the closest file to the edited path wins.\n- Explicit user or maintainer instructions override this file.\n- Do not hand-edit numbered Drizzle migrations or `drizzle/meta/`.\n- Use `pnpm run db:generate` for new migrations, then update fixtures and migration tests.\n- Avoid editing `dist/`, `release/`, `out/`, `build/`, and generated package artifacts\n  unless the task is explicitly about packaging, signing, or release behavior.\n- Do not dispatch release workflows, publish packages, upload artifacts, or trigger\n  external deployments unless the user explicitly asks for release work.\n- Treat `src/main/core/acp/`, `packages/core/src/services/pty/`,\n  `src/core/services/ssh/`, `src/main/db/`, updater code, and provider process\n  spawning as high risk.\n- Read the matching `agents/risky-areas/` page before touching database, PTY, SSH, or\n  updater code.\n- Do not weaken shell quoting, spawn behavior, env allowlists, path validation, or\n  secret redaction casually.\n- Prefer existing service, provider, plugin, Wire, modal, view, tab, and store patterns\n  over new abstractions.\n- New Wire procedures belong in the owning slice's `api/` contract with a controller in\n  `node/`, registered through `src/core/manifests/shared/desktop-wire-contract.ts` and\n  `src/core/manifests/node/controllers.ts`.\n- Keep renderer-main calls on typed Wire contracts, live models, and typed events. The preload\n  bridge should stay small; add direct `window.electronAPI` surface only when an\n  Electron/browser primitive cannot fit the Wire path.\n- Access task and Project MobX stores through selectors and task view hooks:\n  `getTaskStore`, `asProvisioned`, `taskViewKind`, `getTaskManagerStore`,\n  `getProjectStore`, `asAvailableProject`, `getProjectHostAccess`, `useTaskViewKind`, `useWorkspace`,\n  `useWorkspaceId`, `useDevServers`, `useWorkspaceViewModel`, `useConversations`,\n  and `useTerminals`.\n- Never use `asProvisioned(...)!` or `asAvailableProject(...)!`; use explicit null checks.\n- State guards must check `kind !== 'ready'` rather than enumerating non-ready states.\n- Access task managers through `getTaskManagerStore(projectId)`, not `project.taskManager`.\n- Access available Project contexts through `asAvailableProject(getProjectStore(id))`, not inline\n  guards. Read Host-dependent action state through that context's `host` access interface.\n- Task selectors live in `src/core/features/tasks/api/browser/task-state/task-selectors.ts`.\n- Project selectors live in `src/core/features/projects/api/browser/stores/project-selectors.ts`.\n- For provider changes, update plugin metadata, shared provider metadata, ACP support\n  flags, PTY env passthrough if needed, hook integrations, renderer assumptions, and\n  tests for non-standard behavior.\n- For ACP changes, preserve protocol state-machine behavior in `packages/core/src/runtimes/acp/`,\n  keep provider-specific transforms in `packages/plugins/`, and adapt UI payloads at\n  app or chat-UI edges.\n- For MCP changes, keep canonical data in shared types and adapt provider formats at edges.\n- Follow `.github/PULL_REQUEST_TEMPLATE.md`: keep PRs small and focused, self-review\n  before handoff, list checks run, attach UI evidence when applicable, and update docs\n  and tests when behavior changes.\n- Call out high-risk changes explicitly in the PR description or handoff notes,\n  especially database, ACP, PTY, SSH, updater, provider spawning, dependency, and\n  release-related changes.\n- Do not self-approve, merge, assign reviewers, edit branch protection, or change\n  workflow permissions unless the user explicitly asks.\n- Avoid scripted loops against GitHub, Linear, Jira, GitLab, provider CLIs, or release\n  workflows. Use focused queries and respect existing workflow retries; ask before\n  adding polling, bulk API calls, or scheduled automation.\n- Keep automation scoped to the task. Do not run the full local merge gate repeatedly\n  when a focused check is enough during iteration; run broader checks before handoff\n  when the change scope justifies it.\n\n## Extensibility Hooks\n\n- Agent provider plugins live in `packages/plugins/src/agents/impl/` and are registered\n  in `packages/plugins/src/agents/registry.ts`.\n- Provider plugin hosting services live in `packages/core/src/services/agent-plugins/`.\n- Agent provider metadata and capabilities live in `packages/plugins/src/agents/registry.ts`\n  and `packages/plugins/src/agents/impl/`; renderer-facing DTOs are built by\n  `src/core/features/agents/node/agent-payload-builder.ts`.\n- ACP support is exposed through plugin ACP capabilities and portable Core ACP APIs.\n- Provider detection lives in `src/main/core/dependencies/`.\n- Provider PTY behavior and env construction live under `packages/core/src/services/pty/`.\n- Provider event hooks and plugins are installed and hosted by\n `packages/core/src/runtimes/tui-agents/`; desktop projection lives under\n `src/main/core/agent-status/`.\n- ACP process hosting lives in `packages/core/src/runtimes/acp/`; the desktop launches the\n  runtime as a Wire component worker from `src/main/gateway/entries/acp.ts`.\n- Feature modal and view definitions are contributed by slices and aggregated in\n  `src/core/manifests/browser/browser-contributions.ts`; renderer app registries compose host entries.\n- Task tab providers are aggregated through\n  `src/core/manifests/browser/task-tab-contributions.ts`.\n- MCP server config handling lives in `src/core/features/mcp/node/` (wire controller and\n  registration utils), canonical types in `src/core/primitives/mcp/api/`, and UI in\n  `src/core/features/mcp/browser/`.\n- Skills types and validation live in Core primitives; skills UI and service code live in\n  `src/core/features/skills/browser/` and `src/core/features/skills/node/`.\n- Team-owned runtime settings live in each workspace's `.emdash.json`:\n  `preservePatterns`, `scripts.prepare`, `scripts.setup`, `scripts.run`,\n  `scripts.teardown`, and `shellSetup` (a per-workspace override of the host default).\n- Project overrides such as `worktreeRoot`, `defaultBranch`, `baseRemote`, `pushRemote`,\n  `githubAccount`, `agentGitCredentials`, and `tmux` are DB-backed, not `.emdash.json`. Effective\n  tmux resolves through project override > host default > app default; it is not seeded from a\n  default at project creation.\n- The workspace registry owns lifecycle and file-handling config. Host-local personal config\n  (`preservePatterns`, scripts, and auto-run toggles) is versioned JSON on the repository or\n  directory project-root record; worktrees resolve against their project root's personal config.\n- Read registry-owned config through the keyed `projectConfig` live model or `getProjectConfig`.\n  It exposes raw personal/team layers, team-file sources, and one `{ value, from }` result per\n  field. Applicable precedence is personal > that workspace's `.emdash.json` > host default >\n  built-in; arrays replace rather than merge.\n- Per-host defaults (`shellSetup`, `worktreeRoot`, `tmux`) live in the host-settings\n  runtime (`packages/core/src/runtimes/host-settings/`), stored as JSON in the host's\n  emdash data directory and editable from the machines/system settings UI.\n- Desktop project settings compose self-contained domain snapshots: the registry owns\n  lifecycle/file handling, while desktop DB and host-settings domains own Git identity and\n  placement. Forms edit raw layers with explicit per-domain patches; there is no merged settings\n  bag to persist.\n- The scripts runtime is a strict executor: callers must provide resolved `command` and\n  `shellSetup`; it never reads config files or host defaults. The workspace registry resolves and\n  sequences lifecycle commands, then observes runs into durable lifecycle steps.\n- Desktop settings migrations are centralized under\n  `src/core/features/projects/node/settings/migrations/` and run in order when an attachment is\n  established. Legacy schemas and readers stay migration-only; destination markers make imports\n  idempotent and retryable.\n- Optional environment variables include `TELEMETRY_ENABLED`, `EMDASH_DB_FILE`,\n  `EMDASH_DISABLE_NATIVE_DB`, `EMDASH_DISABLE_PTY`, `EMDASH_AUTH_SERVER_URL` (development\n  builds only), `CODEX_SANDBOX_MODE`, and `CODEX_APPROVAL_POLICY`.\n- Build-time telemetry configuration may use `VITE_POSTHOG_KEY` and `VITE_POSTHOG_HOST`.\n- Runtime feature flags are read through telemetry-backed feature flag helpers.\n- App-internal path aliases are defined in `tsconfig.json` (`@/*`, `@core/*`, `@renderer/*`,\n  `@main/*`, `@root/*`, and `@tooling/*`) and mirrored where needed in\n  `electron.vite.config.ts`; workspace packages resolve through their `exports` maps with a\n  `development` condition, not aliases.\n- Versioned JSON column schemas use `defineVersionedSchema()` from\n  `@emdash/core/primitives/versioned-schema/api`\n (`packages/core/src/primitives/versioned-schema/api/versioned-schema.ts`) and Drizzle\n `versionedJsonColumn()` from `src/core/services/app-db/node/versioned-column.ts`.\n\n## Further Reading\n\n- [Agent docs map](agents/README.md)\n- [Quickstart](agents/quickstart.md)\n- [Architecture overview](agents/architecture/overview.md)\n- [Settings ownership and precedence](agents/architecture/settings.md)\n- [Main process architecture](agents/architecture/main-process.md)\n- [Renderer architecture](agents/architecture/renderer.md)\n- [Shared modules](agents/architecture/shared.md)\n- [Developer flows](agents/workflows/flows.md)\n- [Nx task orchestration and caching](agents/workflows/nx.md)\n- [Testing workflow](agents/workflows/testing.md)\n- [Worktrees workflow](agents/workflows/worktrees.md)\n- [Remote development workflow](agents/workflows/remote-development.md)\n- [Workspace server architecture](agents/architecture/workspace-server.md)\n- [Provider integration](agents/integrations/providers.md)\n- [MCP integration](agents/integrations/mcp.md)\n- [IPC conventions](agents/conventions/ipc.md)\n- [Main-process patterns](agents/conventions/main-patterns.md)\n- [Renderer patterns](agents/conventions/renderer-patterns.md)\n- [TypeScript and React conventions](agents/conventions/typescript.md)\n- [Config file rules](agents/conventions/config-files.md)\n- [UI kit conventions](agents/conventions/ui-kit.md)\n- [UI styling conventions](agents/conventions/ui-styling.md)\n- [Versioned schema conventions](agents/conventions/versioned-schemas.md)\n- [Database risk notes](agents/risky-areas/database.md)\n- [PTY risk notes](agents/risky-areas/pty.md)\n- [SSH risk notes](agents/risky-areas/ssh.md)\n- [Updater risk notes](agents/risky-areas/updater.md)\n- [Contributing guide](CONTRIBUTING.md)\n- [Project README](README.md)\n","isInternal":false,"tokens":5638,"sizeBytes":24508},{"name":"CLAUDE.md","path":"CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/CLAUDE.md","title":"Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"@AGENTS.md\n","isInternal":false,"tokens":5,"sizeBytes":11},{"name":"boundaries.md","path":"packages/core/docs/path/boundaries.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/packages/core/docs/path/boundaries.md","title":"Path Skill","category":"anthropic-skill","format":"markdown","content":"# Boundaries\n\nThe path module owns pure identity and lexical path math. It does not own host\nlifecycle, filesystem I/O, authorization, or persistence migrations.\n\n## Conversion Points\n\n```mermaid\nflowchart LR\n  NativeInput[Native string] --> Parser[Path parser]\n  Parser --> Ref[HostFileRef]\n  Ref --> Controller[Domain controller]\n  Controller -->|select by HostRef| Wire[Host-bound Wire connection]\n  Wire --> Runtime[Runtime path key]\n  Runtime --> NativeOutput[Native string]\n  NativeOutput --> Fs[Filesystem or Git]\n```\n\nConvert at these boundaries:\n\n- User input, picker output, SSH responses, and Git roots become structured refs\n  at ingress.\n- Renderer and persistence payloads use `ResourceUri`, `HostFileRef`, or\n  `ScopedPath`, not raw native strings.\n- Domain controllers use `HostRef` to select or relay to a host-bound Wire\n  connection. They do not inject host routing identity into the runtime.\n- Host-bound Wire payloads use `HostAbsolutePath` roots and\n  `PortableRelativePath` coordinates.\n- Host runtimes convert structured paths back to native strings immediately before\n  filesystem, Git, watcher, or process-spawn calls.\n- Watcher and Git events should become `ScopedPath` values before crossing to\n  renderer-facing models.\n\n## Contract Validation\n\nContracts should import schemas from `@emdash/core/path` instead of introducing\nnew `z.string()` path fields once they migrate to structured resources.\n\nRecommended boundary shapes:\n\n- `hostFileRefSchema` for one-off global file resources.\n- `scopedPathSchema` for a single file under a known root.\n- `{ root: hostAbsolutePathSchema, path: portableRelativePathSchema }` for\n  host-bound runtime APIs, tree keys, watcher events, and Git paths.\n- `{ root: hostFileRefSchema, path: portableRelativePathSchema }` while a payload\n  still needs host routing.\n- `resourceUriSchema` for persisted string identity.\n- `absolutePathInputSchema(profile)` for native/user/host input only.\n\nThe schemas delegate to the same parsers as internal code, so contract\nvalidation and local validation enforce the same invariants.\n\n## Lexical Versus Physical Containment\n\n`containsAbsolute()` and `relativizeHostFileRef()` are lexical. They do not\nfollow symlinks.\n\nUse lexical containment for identity, tree projection, deduplication, batching,\nand root-relative path conversion.\n\nUse realpath-based containment for security-sensitive writes, deletes, and\nfilesystem mutation policies. That logic belongs in runtime or app services with\naccess to a filesystem implementation.\n\n## Not Owned Here\n\nThis module intentionally does not implement:\n\n- host connection ownership or mapping from `HostRef` to a live runtime;\n- SSH connection management;\n- workspace mount persistence;\n- `realpath()` or canonical inode checks;\n- renderer tab migrations;\n- Monaco model migration;\n- Wire contract migrations;\n- filesystem authorization.\n\n## Future Adoption Map\n\nExisting consumers can migrate incrementally:\n\n- Files contracts use `HostAbsolutePath` roots and `PortableRelativePath`\n  coordinates; runtime policy adds realpath-based mutation safety.\n- Desktop `RuntimePath` can become a thin native-format adapter around\n  `PathSemantics`.\n- SSH path helpers can use POSIX parsing explicitly instead of app-local string\n  utilities.\n- Git models use repository/worktree `HostAbsolutePath` roots and keep file paths\n  as `PortableRelativePath`.\n- fs-watch can emit root-scoped portable paths, then batch using `ResourceKey`.\n- workspace-server schemas can validate structured refs instead of documenting\n  string conventions only.\n- Renderer tabs, tree nodes, Monaco models, comments, and view-state snapshots\n  can store `ResourceUri` while deriving display paths from workspace mounts.\n","isInternal":false,"tokens":779,"sizeBytes":3730},{"name":"normalization.md","path":"packages/core/docs/path/normalization.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/packages/core/docs/path/normalization.md","title":"Path Skill","category":"anthropic-skill","format":"markdown","content":"# Normalization\n\nPath normalization in Emdash is explicit. Callers choose the dialect they are\nparsing instead of inheriting the operating system of the current process.\n\n## Native Absolute Paths\n\n`parseAbsolute()` accepts a `profile` option.\n\n```ts\nparseAbsolute('/home/david/repo', { profile: { style: 'posix' } });\nparseAbsolute('C:\\\\Users\\\\David\\\\repo', { profile: { style: 'win32' } });\nparseAbsolute('\\\\\\\\server\\\\share\\\\repo', { profile: { style: 'win32' } });\n```\n\nPOSIX parsing treats backslash as a valid filename character. Windows parsing\ntreats both `\\\\` and `/` as separators.\n\nThe parser:\n\n- rejects null bytes;\n- resolves `.` and `..` lexically;\n- rejects paths that escape their root;\n- preserves Unicode spelling by default and normalizes only when requested;\n- preserves path spelling and casing for display;\n- uppercases Windows drive letters in the structured root.\n\nThe parser does not:\n\n- call `realpath()`;\n- check whether the path exists;\n- authorize access;\n- turn a POSIX backslash into a separator.\n\n## Portable Relative Paths\n\n`PortableRelativePath` is used for scoped coordinates such as file tree keys,\nGit paths, watcher events, and root-relative Wire payloads.\n\nRules:\n\n- `/` is the separator.\n- The empty string is the root.\n- Leading `/`, Windows drive roots, and UNC roots are rejected.\n- `.` and `..` are resolved lexically; escaping the root is rejected.\n- Backslash is a literal character, not a separator.\n- Null bytes are rejected.\n\n```ts\nparsePortableRelativePath('src/./components/../index.ts');\n// => 'src/index.ts'\n\nparsePortableRelativePath('../outside');\n// => invalid-path\n```\n\n## Comparison\n\nStored paths preserve spelling. Equality and deduplication use comparison keys\ncreated from an explicit `PathProfile`.\n\n```ts\nconst semantics = createPathSemantics({\n  style: 'win32',\n  caseSensitivity: 'insensitive',\n  unicodeNormalization: 'nfc',\n});\n```\n\nDo not lowercase or otherwise rewrite stored paths to make comparison easier.\nUse `ResourceKey` or `PathSemantics.comparisonKey()` for maps and sets.\n","isInternal":false,"tokens":474,"sizeBytes":2048},{"name":"recipes.md","path":"packages/core/docs/path/recipes.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/packages/core/docs/path/recipes.md","title":"Path Skill","category":"anthropic-skill","format":"markdown","content":"# Recipes\n\n## Parse a Native POSIX Path\n\n```ts\nconst parsed = parseAbsolute('/home/david/repo/src/index.ts', {\n  profile: { style: 'posix' },\n});\n```\n\n## Parse a Native Windows Path\n\n```ts\nconst parsed = parseAbsolute('C:\\\\Users\\\\David\\\\repo\\\\src\\\\index.ts', {\n  profile: { style: 'win32' },\n});\n```\n\n## Build a Host File Ref\n\n```ts\nconst host = hostRef('remote', 'connection-1');\nconst path = parseAbsolute('/repo/src/index.ts', { profile: { style: 'posix' } });\n\nif (path.success) {\n  const ref = hostFileRef(host, path.data);\n}\n```\n\n## Use a Root-Scoped Path\n\n```ts\nconst root = hostFileRef(LOCAL_HOST_REF, rootPath);\nconst relative = parsePortableRelativePath('src/index.ts');\n\nif (relative.success) {\n  const scoped = scopedPath(root, relative.data);\n  const resolved = resolveScopedPath(scoped);\n}\n```\n\nUse this pattern for watcher events, Git paths, file tree nodes, and bulk file\noperations.\n\n## Store a Resource URI\n\n```ts\nconst uri = encodeResourceUri(ref);\nconst decoded = decodeResourceUri(uri);\n```\n\nStore the URI when a durable string identity is needed. Keep `ResourceKey` for\nin-memory maps only.\n\n## Deduplicate with a Resource Key\n\n```ts\nconst key = resourceKeyFromFileRef(ref, {\n  profile: {\n    style: 'win32',\n    caseSensitivity: 'insensitive',\n    unicodeNormalization: 'nfc',\n  },\n});\n\nmodelsByResource.set(key, model);\n```\n\n## Convert an Event Under a Watched Root\n\n```ts\nconst changed = hostFileRef(root.host, changedAbsolutePath);\nconst relative = relativizeHostFileRef(root, changed);\n\nif (relative.success) {\n  const scoped = scopedPath(root, relative.data);\n}\n```\n\nBatch and coalesce watcher events with resource keys derived from the resolved\nfile refs, not by string-prefix comparisons.\n\n## Validate a Contract Model\n\nUse structured schemas for contracts instead of ad hoc `z.string()` path fields.\n\n```ts\nconst readTextInputSchema = z.object({\n  file: scopedPathSchema,\n  options: readFileOptionsSchema.optional(),\n});\n```\n\nFor root-scoped bulk APIs, keeping root and relative path separate is often more\ncompact:\n\n```ts\nconst treePathKeySchema = z.object({\n  root: hostAbsolutePathSchema,\n  path: portableRelativePathSchema,\n  sessionId: z.string(),\n});\n```\n\nThis compact form is appropriate after a domain controller has already selected\nthe host-bound runtime connection. Use `hostFileRefSchema` instead when the same\npayload must still be routed to a host.\n\nFor durable UI state, use a resource URI:\n\n```ts\nconst fileTabStateSchema = z.object({\n  resource: resourceUriSchema,\n});\n```\n\nOnly use native absolute input schemas at ingress boundaries:\n\n```ts\nconst nativePickerInputSchema = z.object({\n  path: absolutePathInputSchema({ profile: { style: 'posix' } }),\n});\n```\n\n## Handle Errors\n\nAll parsing helpers return `Result<T, PathError>`.\n\n```ts\nconst parsed = parsePortableRelativePath(input);\nif (!parsed.success) {\n  switch (parsed.error.type) {\n    case 'invalid-path':\n      return parsed.error.message;\n    case 'outside-root':\n      return 'Path is outside the selected root';\n    default:\n      return 'Invalid resource';\n  }\n}\n```\n","isInternal":false,"tokens":736,"sizeBytes":3079},{"name":"resources.md","path":"packages/core/docs/path/resources.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/packages/core/docs/path/resources.md","title":"Path Skill","category":"anthropic-skill","format":"markdown","content":"# Resources\n\n`HostFileRef` is the canonical application-level file identity:\n\n```ts\ntype HostFileRef = {\n  host: HostRef;\n  path: HostAbsolutePath;\n};\n```\n\nIt identifies a logical path on a host. It is not an inode identity. Symlinks,\nhard links, and renames are handled by runtime-specific filesystem behavior.\n\n## Host References\n\n`HostRef` is defined by `@emdash/core/primitives/host/api`:\n\n```ts\ntype HostRef = {\n  type: 'local' | 'remote';\n  id: string;\n};\n```\n\nIt is used by domain controllers to select a host-bound runtime connection. A\nruntime executes on the addressed host and therefore does not need `HostRef` in\nits own Git or Files keys.\n\n## Absolute Roots\n\nStructured roots avoid ambiguity across hosts and drives:\n\n```ts\n{ kind: 'posix' }\n{ kind: 'drive', driveLetter: 'C' }\n{ kind: 'unc', server: 'server', share: 'share' }\n```\n\nThe path segments are stored separately from the root, so `/`, `C:/`, and\n`//server/share` cannot be confused.\n\n## Scoped Paths\n\nUse `ScopedPath` when many operations share a root:\n\n```ts\ntype ScopedPath = {\n  root: HostFileRef;\n  relative: PortableRelativePath;\n};\n```\n\nThis is the preferred globally addressable shape for watcher events, tree\nentries, and batch operations. Host-local runtime contracts use the equivalent\ncompact shape: a `HostAbsolutePath` root plus `PortableRelativePath` coordinates.\n\n## Resource URIs\n\n`ResourceUri` is the stable string encoding:\n\n```text\nemdash-file://v2/local/local/posix/home/david/repo/src/index.ts\nemdash-file://v2/remote/connection-1/drive/c/Users/David/repo/src/index.ts\nemdash-file://v2/remote/connection-2/unc/server/share/repo/src/index.ts\n```\n\nUse it for serialization, persistence, Monaco model identity, and durable\nmessages. Percent encoding is applied per path segment.\n\n## Contract Schemas\n\nPath schemas live next to the path primitives so Wire/RPC contracts and\npersisted models do not duplicate validation rules.\n\nUse:\n\n- `hostFileRefSchema` for a globally addressable file resource.\n- `scopedPathSchema` for a file under a known root.\n- `portableRelativePathSchema` for Git paths, file tree entry keys, and watcher\n  paths that are already scoped by another field.\n- `resourceUriSchema` for persisted or Monaco-style string identity.\n- `resourceRefFromUriSchema` when a boundary accepts a URI string but internal\n  code wants a decoded `HostFileRef`.\n- `absolutePathInputSchema(profile)` only at native/user/host ingress\n  boundaries.\n\nSchemas delegate to the canonical parsers. For example,\n`portableRelativePathSchema` transforms `src/./index.ts` into `src/index.ts`\ninstead of merely branding the original string.\n\n## Resource Keys\n\n`ResourceKey` is an opaque comparison key for maps and deduplication. It may be\ncase-insensitive or Unicode-normalized depending on the selected profile.\n\nDo not persist `ResourceKey` as the only representation of a file. It is lossy by\ndesign and may not preserve display spelling.\n","isInternal":false,"tokens":694,"sizeBytes":2924},{"name":"clients.md","path":"packages/wire/docs/api/clients.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/packages/wire/docs/api/clients.md","title":"Api Skill","category":"anthropic-skill","format":"markdown","content":"# Typed Clients\n\n`client(contract, connection)` creates a typed `ContractClient`. It has the\nsame nested shape as the contract, but it does not materialize live state.\n\nThink of it as a type-safe wrapper around `Connection`: it knows contract paths,\ninput/output types, live topic ids, group keys, and mutation envelopes, but it does\nnot keep snapshots, apply patches, or settle mutations against local state.\n\n```ts\nconst contractClient = client(api, connect(transport));\n\nawait contractClient.ping({ message: 'hello' });\n\nconst state = contractClient.conversation.state(key, 'state');\nconst snapshot = await state.snapshot();\nconst detach = await state.attach((update) => {\n  console.log(update.delta);\n});\n```\n\n## Endpoint Shapes\n\nEach contract endpoint becomes a protocol client handle:\n\n- Procedures become typed functions: `contractClient.ping(input, { signal? })`.\n- Live logs become `LiveLogClientHandle`s: `contractClient.output.handle(key)`.\n- Event streams become `EventStreamClientHandle`s:\n  `await contractClient.events.subscribe(key, { onEvent, onGap?, onError? })`.\n- Live models become `LiveModelClientHandle`s:\n  `contractClient.conversation.state(key, 'state')` and\n  `contractClient.conversation.mutate('setTitle', envelope)`.\n- Jobs become `LiveJobClientHandle`s: `contractClient.build.start(input)`,\n  `contractClient.build.cancel(jobId)`, and `contractClient.build.handle(jobId)`.\n\n`LiveClientHandle` is the common live handle:\n\n```ts\ntype LiveClientHandle<T> = {\n  topic: string;\n  snapshot(): Promise<LiveSnapshot<T>>;\n  attach(\n    push: (update: LiveUpdate) => void,\n    options?: {\n      onReattach?: () => void;\n      onReattachError?: (error: WireError, context: { retrying: boolean }) => void;\n    }\n  ): Promise<Unsubscribe>;\n  asLiveSource(): LiveSource;\n};\n```\n\n`snapshot()` and `attach()` are useful for low-level code. For example, PTY output\ncan stream directly into an xterm instance without allocating another store:\n\n```ts\nconst output = contractClient.ptyOutput.handle({ sessionId });\nterm.write((await output.snapshot()).data.text);\n\nconst detach = await output.attach((update) => {\n  term.write((update.delta as { chunk: string }).chunk);\n}, {\n  onReattach: async () => {\n    term.reset();\n    term.write((await output.snapshot()).data.text);\n  },\n  onReattachError: (error, { retrying }) => {\n    if (!retrying) showOutputError(error.message);\n  },\n});\n```\n\nWhen a process wants local state, pass the client handle to a replica wrapper instead.\n\n## Forwarding\n\nContract clients are intentionally forwardable. Use `forwardController(contract, client)`\nwhen a hop should serve the same contract without owning state or intercepting\nindividual endpoints:\n\n```ts\nconst upstream = client(api, connect(sshTransport));\nconst controller = forwardController(api, upstream);\n```\n\nNo live state is created at that hop. The downstream client sees the same contract,\nwhile calls, snapshots, live attachments, jobs, and group mutations are forwarded to\nthe upstream connection. Event stream handles are forwarded as live attachments with\nthe same at-most-once delivery semantics as the upstream connection.\n\nForwarding also preserves upstream live lifecycle. A downstream subscriber receives\n`onGap` after the upstream attachment has reattached, and receives `onError` when\nan upstream reattach fails, even if the downstream transport never disconnected.\n\nThis is the right shape for protocol relays and middle tiers that should not own or\ncache state. The hop does not create `ReplicaState`s, does not allocate local\ncursor spaces, and does not run mutation settling. It just preserves the contract\nsurface while delegating to the upstream connection.\n\n`forwardController()` is the preferred shape for full protocol relays because it\nderives the controller implementation from the contract. Adding a new endpoint to\nthe contract does not require a hand-written forwarding entry at each relay. File\ndownloads are adapted internally from the client download handle back into the\ncontroller's streamed download shape.\n\nUse `createController()` directly when forwarding should be selective. A procedure\nmethod can be passed as a procedure implementation, and live client handles can be\npassed as live model, live log, event stream, or live job implementations:\n\n```ts\nconst upstream = client(workspaceApi, connect(sshTransport));\n\nconst controller = createController(workspaceApi, {\n  // Local interception.\n  ping: async () => 'desktop-main',\n\n  // Forwarded subtree.\n  git: upstream.git,\n\n  // Cached group; see Replicas.\n  conversation: createLiveModelReplicaCache(\n    workspaceApi.conversation,\n    upstream.conversation,\n    { lingerMs: 10 * 60_000 }\n  ),\n});\n```\n\nUse this composition style for middle tiers that need local handlers, auditing,\nauthorization checks, replicas, or other endpoint-specific behavior. The explicit\ncontroller implementation makes the interception points visible.\n\n## When to Use Replicas\n\nUse replicas when a process wants local state. See [Replicas](../live/replicas.md).\n\nCommon examples:\n\n- A renderer window wants observable UI state.\n- Electron main wants state that survives renderer reloads.\n- A process wants a local cache before serving the same live model contract to\n  downstream clients.\n\nFor Electron main and other middle tiers that both serve and inspect live state, use\nthe replica local access pattern in\n[Replicas](../live/replicas.md#model-replicas).\n\nDo not materialize just to forward. Forward the client handle or subtree directly.\n","isInternal":false,"tokens":1186,"sizeBytes":5516},{"name":"contracts.md","path":"packages/wire/docs/api/contracts.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/packages/wire/docs/api/contracts.md","title":"Api Skill","category":"anthropic-skill","format":"markdown","content":"# API Contracts\n\nContracts describe the API surface once. Controller creation and client creation\nderive their types from the same object.\n\n## Defining a Contract\n\nUse `defineContract({ ... })`. Object keys are significant: they determine\nprocedure paths and live ref ids after nested contracts are mounted.\n\n```ts\nexport const notesApi = defineContract({\n  activity: liveLog({ key: sessionKeySchema }),\n  session: liveModel({\n    key: sessionKeySchema,\n    states: {\n      notes: liveState({ data: notesStateSchema }),\n    },\n    mutations: {\n      addNote: mutation(\n        { input: z.object({ text: z.string() }), data: noteSchema, error: z.string() },\n        (ctx, input) => {\n          const note = { id: input.text.toLowerCase(), text: input.text };\n          ctx.produce('notes', (draft) => {\n            draft.notes.push(note);\n          });\n          return ok(note);\n        }\n      ),\n    },\n  }),\n  clearNotes: procedure({\n    input: sessionKeySchema,\n    output: notesStateSchema,\n  }),\n});\n```\n\nSee [../../examples/api-definition/contract.ts](../../examples/api-definition/contract.ts).\n\n## Endpoint Kinds\n\n### `procedure`\n\n`procedure({ input, output })` defines a request/response call. Procedure clients\naccept an optional `{ signal }` call option for cooperative cancellation; see\n[serving](./serving.md#cancellation).\n\n### `fallible`\n\n`fallible({ input, data, error })` defines a procedure whose output is a\n`Result<data, error>` payload:\n\n```ts\nloadNote: fallible({\n  input: z.object({ id: z.string() }),\n  data: noteSchema,\n  error: z.object({ type: z.literal('note_not_found') }),\n});\n```\n\nUse `fallible()` for expected domain failures that callers should handle as data.\nThrown `WireError`s are reserved for infrastructure failures and bugs such as\ndisconnects, unknown paths, and uncaught handler exceptions. See\n[wire errors](./errors.md).\n\n### `mutation`\n\n`mutation({ input, data, error }, handler?)` defines a live-state mutation shape.\nMutations are members of `liveModel()` definitions. Optimistic previews should use\nshared reducer functions with the `optimistic()` bridge rather than contract-local\nclient models.\n\n`mutation()` is for operations that must settle live model cursors. Use\n`procedure()` for calls that do not update live models and `liveJob()` for\nlong-running work.\n\n### `liveLog`\n\n`liveLog({ key })` declares a keyed text log endpoint. It is served by a\n`LiveLogSource` resolver and consumed through a `LiveLogClientHandle` or a\n`LiveLogReplicaCache`.\n\n### `eventStream`\n\n`eventStream({ key, event })` declares a keyed, fire-and-forget event channel.\nEvents are delivered only while a client is attached; the server keeps no\nretained snapshot and late subscribers do not receive historical events.\n\n```ts\nfileEvents: eventStream({\n  key: z.object({ rootPath: z.string() }),\n  event: z.object({\n    kind: z.enum(['create', 'update', 'delete']),\n    path: z.string(),\n  }),\n});\n```\n\nServe event streams with `createEventStreamHost(contract.fileEvents)` or a\nresolver that returns an `EventStreamSource`. Client handles expose\n`await subscribe(key, { onEvent, onGap?, onError? })`. `onGap` runs after\ntransport reattach so consumers can resync state that may have missed events.\nUse `eventStream` for loss-tolerant notifications and invalidation signals; use\n`liveLog` when callers need retained text output and `liveModel` when callers\nneed convergent state. See [event streams](../live/event-stream.md).\n\n### `liveJob`\n\n`liveJob({ input, progress, result, error })` models long-running work with progress,\ncancellation, terminal state, and reattach:\n\n```ts\nbuild: liveJob({\n  input: z.object({ target: z.string() }),\n  progress: z.object({ step: z.string() }),\n  result: z.object({ artifact: z.string() }),\n  error: z.object({ message: z.string() }),\n});\n```\n\n`createController()` binds the endpoint to `{ run, toError? }`. `run` returns a\n`Result<result, error>` payload; `toError` is only an optional fallback for\nunexpected thrown errors. The typed client gets `start(input)`, `cancel(jobId)`,\nand `handle(jobId)` helpers. See\n[live jobs](../live/live-job.md).\n\n### `downloadFile` and `uploadFile`\n\n`downloadFile({ input, meta?, error })` and\n`uploadFile({ input, accept?, maxSize?, result, error })` model byte transfer as a\ntyped call envelope plus a credit-based blob channel. Bytes are streamed in\nbounded chunks and are not embedded in procedure inputs or results:\n\n```ts\nreadAttachment: downloadFile({\n  input: z.object({ id: z.string() }),\n  error: z.object({ type: z.string() }),\n});\n\nuploadAttachment: uploadFile({\n  input: z.object({ conversationId: z.string() }),\n  accept: ['image/png', 'image/jpeg'],\n  maxSize: 25 * 1024 * 1024,\n  result: attachmentRefSchema,\n  error: z.object({ type: z.string() }),\n});\n```\n\nDownload clients receive a single-use handle with `chunks()`, `bytes()`, `file()`,\nand `cancel()`. Upload clients pass a file-like value as the second argument. See\n[file endpoints](./files.md).\n\n### `liveModel`\n\n`liveModel({ key, states, mutations })` aggregates related live states and the\nmutations that may touch them. A single key addresses every member. States are\ndeclared with `liveState({ data })`; the helper creates keyed member refs.\n\n```ts\nconst api = defineContract({\n  conversation: liveModel({\n    key: conversationKeySchema,\n    states: {\n      state: liveState({ data: stateSchema }),\n      usage: liveState({ data: usageSchema }),\n    },\n    mutations: {\n      setTitle: mutation(\n        { input: z.object({ title: z.string() }), data: z.void(), error: z.string() },\n        (ctx, input) => {\n          ctx.produce('state', (draft) => {\n            (draft as { title: string }).title = input.title;\n          });\n          ctx.produce('usage', (draft) => {\n            (draft as { tokens: number }).tokens += input.title.length;\n          });\n          return ok(undefined);\n        }\n      ),\n    },\n  }),\n});\n```\n\nInline group mutation handlers should be pure functions of the member drafts and\ninput: avoid I/O, time, randomness, and server-only state in the inline handler\nbody. If a mutation is schema-only (`mutation(def)`), the server supplies the\nhandler when it creates the live model host.\n\n## Nested Composition\n\nContracts can contain other contracts:\n\n```ts\nconst ptyAgent = defineContract({\n  sessions: liveModel({\n    key: sessionKeySchema,\n    states: { state: liveState({ data: sessionStateSchema }) },\n  }),\n  output: liveLog({ key: sessionKeySchema }),\n});\n\nconst api = defineContract({ ptyAgent });\n```\n\nThe final mount path determines ids and procedure paths:\n\n- `api.ptyAgent.sessions.states.state.id` is `ptyAgent.sessions.state`.\n- `api.ptyAgent.output.id` is `ptyAgent.output`.\n- a procedure at `api.ptyAgent.startSession` is called as `ptyAgent.startSession`.\n- a group member at `api.tasks.conversation.states.state` gets the id\n  `tasks.conversation.state`.\n\nThis lets packages define small contracts locally and compose them into a larger\nworkspace API without an extra namespace argument.\n\n## Live Model Providers\n\nOn the server, implement a live model endpoint with a `LiveModelProvider`. The\nusual way to build one is `expose()` from `@emdash/wire/state`, which resolves\neach contract state to a kernel readable per key:\n\n```ts\nconst conversations = expose(\n  api.conversation,\n  {\n    state: (key) => conversationState(key),\n    usage: (key) => conversationUsage(key),\n  },\n  { mutations }\n);\n```\n\nThen bind the endpoint by passing the provider in the implementation object:\n\n```ts\nconst controller = createController(api, { conversation: conversations });\n```\n\nThe live model client handle exposes `state(key, name)` for member handles and\n`mutate(name, envelope)` for mutation calls. Wrap it in `createLiveModelReplicaCache()`\nwhen a process wants local state, ref counting, and mutation settling. See\n[serving](./serving.md#typed-clients).\n","isInternal":false,"tokens":1859,"sizeBytes":7869},{"name":"errors.md","path":"packages/wire/docs/api/errors.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/packages/wire/docs/api/errors.md","title":"Api Skill","category":"anthropic-skill","format":"markdown","content":"# Wire Errors\n\nWire has two error planes:\n\n- Expected domain failures travel as `Result` payloads. Prefer `fallible()` for\n  procedure contracts whose callers should handle failures as ordinary data.\n- Thrown `WireError`s are reserved for infrastructure failures, contract drift,\n  lifecycle mistakes, and uncaught handler exceptions.\n\n`WireError` instances carry a typed `code`, a message, and an optional serialized\ncause. When an uncaught JavaScript error crosses the wire, the transport serializes\nit as `HANDLER_ERROR` with a clone-safe cause.\n\n## Codes\n\n### `CANCELLED`\n\nThe call was aborted. The client raises it when the caller's `AbortSignal` is\nalready aborted or aborts while a call is pending. The server raises it when it\nreceives a `cancel` message and the handler observes the abort.\n\nRetrying is not automatic and usually not appropriate because cancellation was\ncaller intent.\n\n### `DISCONNECTED`\n\nThe transport disconnected while a request was in flight, posting a request\nfailed because the peer was already gone, the held-call buffer overflowed while\ndisconnected, or the transport failed permanently (the terminal cause is\nattached as `cause`).\n\nMutations are never retried automatically. Callers can opt in per call with\n`retry: { schedule }`; that retry is safe because mutations carry a stable\n`mutationId` and the server deduplicates repeated ids.\n\n### `UNKNOWN_PROCEDURE`\n\nNo procedure handler exists for the call path. Common causes are an incorrect\nclient path, a missing implementation branch in `createController()`, or a drift\nbetween the contract and the controller implementation.\n\nThis is not retryable without changing code or wiring.\n\n### `UNKNOWN_TOPIC`\n\nThe live topic's ref id is not registered by the controller.\n\nThis usually means the client encoded the wrong live ref id, or a controller was\ncreated without the endpoint that owns the ref. It is not retryable without\nchanging the topic or server wiring.\n\n### `NOT_FOUND`\n\nThe live ref id is known, but no live instance exists for the provided key.\nExamples include a live model group instance that has not been created yet or a\njob state that has already been evicted.\n\nRetrying may make sense only if the domain resource is expected to appear later.\n\n### `MISSING_HANDLER`\n\nController creation-time error: a contract endpoint has no implementation. Examples include a\nmissing procedure implementation, missing live resolver, missing job handler, or\nmissing live model provider.\n\nThis should not cross the wire during normal operation. Fix the controller implementation.\n\n### `CONTRACT_MISMATCH`\n\nController creation-time error: a live model provider, replica cache, or client\nhandle was created for a different contract than the endpoint being bound.\n\nThis is not retryable; fix the provider/contract pairing.\n\n### `ALREADY_EXISTS`\n\nA keyed live resource was created for a key that already has a live instance.\n\nThis is a lifecycle error. Dispose or reuse the existing instance instead of\ncreating another one for the same key.\n\n### `TIMEOUT`\n\nA Wire call or snapshot exceeded an infrastructure deadline. The connection\napplies a default 30s call deadline (`callTimeoutMs`, per-call `timeoutMs`\noverride) spanning call-issued to result-received, including time spent held\nwhile disconnected; live attach traffic and blob streaming are exempt. Timeout\nis distinct from `CANCELLED`: cancellation is caller intent, while timeout means\npolicy stopped waiting for the operation.\n\nTimeouts are not retried automatically. Mutation calls that opt in to\n`retry: { schedule }` retry both `DISCONNECTED` and `TIMEOUT`, relying on\nstable `mutationId` deduplication. If a domain treats a timeout as an expected\noutcome, model it as a `Result` payload instead of relying on the\ninfrastructure error plane.\n\n### `HANDLER_ERROR`\n\nAn uncaught exception escaped a server handler or validation boundary. Expected\ndomain failures should be modeled as `Result` payloads with `fallible()` instead\nof thrown.\n\nThe serialized wire error includes the thrown value as a clone-safe cause. Treat\nthis as a bug signal and fix the handler or validation logic.\n\n## Live Attachment Errors\n\nLive attachments use the same infrastructure error plane as calls and snapshots.\nThey do not wrap startup or lifecycle failures in endpoint `Result` payloads.\n\nInitial attachment failures reject the `attach()` or event-stream `subscribe()`\npromise with a `WireError`. Common codes are `DISCONNECTED`, `UNKNOWN_TOPIC`,\n`NOT_FOUND`, and `HANDLER_ERROR`.\n\nOnce an attachment is established, the client keeps it as durable intent across\ntransport reconnects. Reattach failures are delivered to the subscription's\n`onReattachError` or `onError` callback with `{ retrying }`:\n\n- `DISCONNECTED` is reported with `retrying: true`; the attachment stays\n  registered and will try again on the next reconnect.\n- Other `WireError`s are reported with `retrying: false`; the attachment is\n  terminated because retrying the same topic would repeat the same failure.\n\nForwarding preserves these lifecycle notifications. A downstream connection can\nremain healthy while an upstream live attachment reports a gap or terminal error.\nForwarding sends those as live lifecycle messages rather than hiding them behind\nthe downstream transport state.\n\nThese errors are distinct from expected domain events carried by a stream. For\nexample, fs-watch may emit `{ kind: 'error' }` as part of its event payload when a\nnative watcher fails to start. That payload belongs to the fs-watch contract. A\nlive attachment `WireError` means the wire topic itself could not attach or\nreattach.\n","isInternal":false,"tokens":1165,"sizeBytes":5609},{"name":"files.md","path":"packages/wire/docs/api/files.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/packages/wire/docs/api/files.md","title":"Api Skill","category":"anthropic-skill","format":"markdown","content":"# File Endpoints\n\n`downloadFile()` and `uploadFile()` stream bytes through a dedicated blob channel\ninstead of embedding `Uint8Array` payloads in procedure inputs or results. The call\npayload carries only typed input, metadata, and a channel id; byte chunks flow after\nthat over credit-based frames.\n\n## Contracts\n\n```ts\nconst api = defineContract({\n  readAttachment: downloadFile({\n    input: z.object({ id: z.string() }),\n    error: z.object({ type: z.string() }),\n  }),\n  uploadAttachment: uploadFile({\n    input: z.object({ conversationId: z.string() }),\n    accept: ['image/png', 'image/jpeg'],\n    maxSize: 25 * 1024 * 1024,\n    result: attachmentRefSchema,\n    error: z.object({ type: z.string() }),\n  }),\n});\n```\n\nFile metadata uses `{ name, mimeType, size?, lastModified? }`. Download endpoints\nmay extend the metadata schema with endpoint-specific fields.\n\n## Server Implementations\n\n```ts\nconst controller = createController(api, {\n  readAttachment: async ({ id }) => {\n    const stored = await store.get(id);\n    if (!stored) return err({ type: 'missing' });\n    return ok({\n      meta: {\n        name: stored.ref.name,\n        mimeType: stored.ref.mimeType,\n        size: stored.data.byteLength,\n      },\n      source: stored.stream(),\n    });\n  },\n  uploadAttachment: async (input, file) => {\n    const bytes = await file.bytes();\n    return ok(await store.put({ data: bytes, mimeType: file.mimeType, name: file.name }));\n  },\n});\n```\n\nThe source can be an `AsyncIterable<Uint8Array>` or a readable stream-like value.\n`WireFile` on upload exposes `stream()`, `bytes()`, `file()`, and `cancel()`.\n\n## Client Usage\n\n```ts\nconst downloaded = await client.readAttachment({ id });\nif (downloaded.success) {\n  for await (const chunk of downloaded.data.chunks()) {\n    // incremental processing\n  }\n}\n\nconst uploaded = await client.uploadAttachment({ conversationId }, file);\n```\n\nDownload handles are single-consumption: choose `chunks()`, `bytes()`, or `file()`.\n`bytes()` verifies `meta.size` when present.\n\n## Protocol Invariants\n\n- Bytes are sent as `blob-chunk` frames and never inside `call`/`result` values.\n- Producers send only after the consumer grants credit with `blob-pull`.\n- Consumers keep a bounded local chunk mailbox aligned with the maximum granted\n  credit. Overflow is treated as a protocol failure; blob chunks are never\n  dropped.\n- `blob-end` is the only successful EOF signal; disconnect before it is an error.\n- `blob-close` cancels a channel and disposes the producer source.\n- Frames for unknown channels are ignored so close/chunk races are harmless.\n- Chunks are re-sliced to standalone buffers before posting to avoid cloning a much\n  larger backing `ArrayBuffer`.\n\nStructured-clone transports carry `Uint8Array` chunks natively. `streamTransport`\nuses length-prefixed binary framing so workspace-server stdio/socket/SSH paths send\nraw bytes with no base64 expansion.\n","isInternal":false,"tokens":678,"sizeBytes":2907},{"name":"middleware.md","path":"packages/wire/docs/api/middleware.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/packages/wire/docs/api/middleware.md","title":"Api Skill","category":"anthropic-skill","format":"markdown","content":"# Composable Middleware\n\nWire uses explicit composition for execution policy. Contracts describe protocol\nshape; middleware wraps handlers or controllers at the composition site.\n\n```ts\nimport { compose, deduplicate, withRetry, withTimeout } from '@emdash/shared/requests';\nimport { retrySchedules } from '@emdash/shared/scheduling';\nimport { createController } from '@emdash/wire/rpc';\n\nconst loadStats = compose(\n  async (input: { repo: string }, meta: { signal?: AbortSignal }) => {\n    return await fetchStats(input.repo, { signal: meta.signal });\n  },\n  [\n    withTimeout({ timeoutMs: 20_000 }),\n    withRetry({\n      schedule: retrySchedules.exponential({\n        initialMs: 250,\n        maxMs: 2_000,\n        maxRetries: 3,\n      }),\n      shouldRetry: isTransient,\n    }),\n    withTimeout({ timeoutMs: 5_000 }),\n  ]\n);\n\nconst controller = createController(api, { loadStats });\n```\n\n## `compose()`\n\n`compose(target, middlewares)` accepts the value being wrapped first and a\nreadonly middleware array second:\n\n```ts\ntype Middleware<T> = (next: T) => T;\n```\n\nThe first array entry is outermost. It sees the request first and settles last:\n\n```mermaid\nflowchart LR\n  composeCall[\"compose(handler, middlewares)\"] --> totalTimeout[TotalTimeout]\n  totalTimeout --> retry[Retry]\n  retry --> attemptTimeout[AttemptTimeout]\n  attemptTimeout --> handler[RawHandler]\n```\n\nThis order makes policy boundaries explicit. In the first example, the outer\ntimeout bounds the complete retry program, while the inner timeout bounds each\nindividual attempt.\n\n## Handler Middleware\n\nHandler middleware wraps procedure implementations before `createController()`:\n\n```ts\nconst handler = compose(rawHandler, [\n  withTimeout({ timeoutMs: 10_000 }),\n  deduplicate({ key: (input) => input.repo }),\n]);\n```\n\nUse handler middleware for endpoint-specific execution policy:\n\n- Timeout and deadline policy.\n- Retry policy for idempotent or externally safe operations.\n- In-flight request deduplication.\n- Per-endpoint rate limiting or concurrency limits when those utilities exist.\n\nProcedure handlers receive `(input, meta)`. Middleware must preserve every field\non `meta` and replace only `meta.signal` when it needs derived cancellation:\n\n```ts\nconst wrapped = async (input, meta) => {\n  return await next(input, { ...meta, signal: derivedSignal });\n};\n```\n\nMiddleware functions are intentionally not contract annotations. Two endpoints can\nshare the same schema and still have different execution policy depending on\nwhere they are served.\n\n## Controller Middleware\n\nController middleware wraps a complete `Controller` and satisfies\n`Middleware<Controller>` from `@emdash/shared/requests`. The seam exists as the\nsanctioned extension point for future cross-cutting concerns (for example\nobservability), even though Wire ships no controller middleware today.\n\nContract validation is not middleware: it is a `createController()` option.\nEvery controller validates by default — `'full'` (inputs + outputs) outside\nproduction, `'inputs'` in production — with an explicit `validate` override:\n\n```ts\nconst controller = createController(api, impl, { validate: 'inputs' });\n```\n\nUse controller middleware for boundary-wide behavior:\n\n- Logging and request instrumentation.\n- Session or authorization checks that apply to every procedure on the served\n  controller.\n\nPrefer handler middleware when the policy belongs to one endpoint. Prefer\ncontroller middleware when the policy belongs to the served boundary.\n\n## Timeout\n\n`withTimeout({ timeoutMs, clock? })` from `@emdash/shared/requests` is handler\nmiddleware backed by Shared `runWithTimeout()` from `@emdash/shared/scheduling`.\nIt derives a child `AbortSignal`, passes that signal to the wrapped handler, and\nconverts an expired deadline to a timeout error.\n\n```ts\nconst startSession = compose(rawStartSession, [\n  withTimeout({ timeoutMs: 10_000, clock }),\n]);\n```\n\nTimeout is infrastructure failure, not a domain result. If callers should handle\na timeout as expected data, model it in the endpoint's `Result` error schema\ninstead.\n\nCaller cancellation still wins over timeout. If the caller aborts the request,\nthe wire call rejects with `CANCELLED`.\n\n## Retry\n\n`withRetry()` from `@emdash/shared/requests` is handler middleware backed by\nShared `retry()` from `@emdash/shared/scheduling`. It requires an explicit\n`shouldRetry` classifier:\n\n```ts\nconst loadRemote = compose(rawLoadRemote, [\n  withRetry({\n    schedule: retrySchedules.fixed(250, 2),\n    shouldRetry: (error) => isWireError(error, 'DISCONNECTED'),\n  }),\n]);\n```\n\nDo not retry arbitrary procedure failures. A retry policy should be attached only\nwhen the operation is idempotent, deduplicated by request identity, or otherwise\nsafe to repeat. Contract mutations already have their own retry behavior and\nretry only `DISCONNECTED` by default.\n\n## Deduplication\n\n`deduplicate(options?)` from `@emdash/shared/requests` is handler middleware for\nsharing one in-flight execution for identical inputs:\n\n```ts\nconst expensiveStats = compose(rawExpensiveStats, [\n  deduplicate({ key: (input) => `${input.repo}:${input.branch}` }),\n]);\n```\n\nThe default key is stable JSON identity. Settled calls are not cached and\nrejections are not cached.\n\nCaller cancellation is independent from shared execution. One caller may stop\nwaiting without aborting the underlying request for other waiters. Set\n`cancelWhenUnused: true` only when it is correct to abort shared work after the\nfinal waiter leaves.\n\n## What Stays Out Of Middleware\n\nKeep protocol-visible behavior in contracts:\n\n- Endpoint kind.\n- Input and output schemas.\n- Upload size and MIME constraints.\n- Live model, log, stream, and job semantics.\n- Mutation idempotency envelopes.\n\nKeep UI presentation policy, such as command visibility or context keys, outside\nWire middleware. Keep domain failures in `Result` payloads when callers are\nexpected to handle them.\n","isInternal":false,"tokens":1290,"sizeBytes":5906},{"name":"serving.md","path":"packages/wire/docs/api/serving.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/packages/wire/docs/api/serving.md","title":"Api Skill","category":"anthropic-skill","format":"markdown","content":"# Serving and Clients\n\nThe API layer turns a contract into a server-side `Controller`, serves it over a\n`WireTransport`, and creates a typed client over the matching transport.\n\n## Creating a Controller\n\n`createController(contract, impl)` maps each endpoint to server behavior:\n\n```ts\nconst controller = createController(\n  notesApi,\n  {\n    session: sessionsHost,\n    activity: () => activityLogServer,\n    clearNotes: (input) => {\n      instance.states.notes.produce((draft) => {\n        draft.notes = [];\n      });\n      return instance.states.notes.snapshot().data;\n    },\n  }\n);\n```\n\nThe `impl` object is keyed by the contract shape. Procedures receive\n`(input, meta)`. Jobs use `{ run, toError? }`, a `LiveJobClientHandle`, or a\n`LiveJobReplicaCache`. Live logs use resolver functions, `LiveLogClientHandle`s, or\n`LiveLogReplicaCache`. Event streams use `createEventStreamHost()`, resolver functions,\nor `EventStreamClientHandle`s. Live model contracts use `expose()` or an existing\n`LiveModelClientHandle`.\n\nMutation idempotency is configured on `expose()`, not on the\ncontroller. The controller only routes calls, snapshots, attachments, and live\nmutation procedure envelopes.\n\nLive model state is separate from the contract because instances are runtime\nresources. A controller can be created once, while conversations, sessions, or\nwindows create and dispose keyed state through `family()` and scoped `expose()`\nlifecycles.\n\nSee the state bridge tests under `src/state/bridge/`.\n\n## Serving\n\n`serve(transport, controller, options?)` listens for protocol messages:\n\n- `call` invokes `controller.call(path, input, meta)`.\n- `snapshot` calls `LiveSource.snapshot()`.\n- `attach` subscribes to a live source and forwards `update` messages.\n- `detach` unsubscribes.\n- `cancel` aborts an in-flight call by id.\n\n```ts\nconst pair = memoryTransportPair();\nconst stop = serve(pair.right, controller, {\n  logger,\n  instrumentation: { callEnd: (event) => logger.debug('call finished', event) },\n});\n```\n\n`serve()` returns an unsubscribe. Call it when the transport or server session\ngoes away. It also aborts in-flight calls and detaches live subscriptions when\nthe transport disconnects or when the serve loop is disposed.\n\n## Validation\n\nContract validation is built into `createController()`. Every controller\nvalidates by default, and the default is environment-sensitive: `'full'`\n(inputs + outputs) everywhere except production, `'inputs'` in production\n(`NODE_ENV === 'production'`). Pass `validate` to override:\n\n```ts\nconst controller = createController(notesApi, impl, { validate: 'inputs' });\nconst stop = serve(pair.right, controller);\n```\n\nPolicies:\n\n- `none`: no schema parsing.\n- `inputs`: parse procedure inputs, upload/download inputs, live model mutation\n  envelopes, job start/cancel inputs, and live topic keys before delegating.\n- `full`: includes `inputs`, then also parses procedure outputs, upload/download\n  results, mutation results, and live job start results.\n\nThe default rule applies identically to bare controllers, component instances,\ncomponent workers, and tests (`createTestWire`), so a contract violation in a\ntest fails the same way it would in dev. Production boundaries stay on\n`inputs`: values crossing a trust boundary are always parsed, while output\nvalidation is skipped where it is too expensive.\n\nValidation covers request/response values that pass through\n`Controller.call()` and live topic keys passed to `Controller.resolveLive()`.\nLive job progress, result, error values, and event stream payloads are emitted\nlater as live updates and are not intercepted.\n\n## Connecting\n\n`connect(transport, options?)` creates a low-level `Connection`:\n\n```ts\nconst connection = connect(pair.left, { instrumentation });\n```\n\n`Connection` supports:\n\n- `call(path, input, { signal? })`.\n- `snapshot(topic)`.\n- `attach(topic, push, { signal?, onReattach? })`.\n- `onDisconnect(cb)`.\n- `dispose()` to release the logical connection without closing its transport.\n\nOn disconnect, pending calls reject with `WireError` code `DISCONNECTED`.\nExisting attachments are retained locally. If the transport exposes\n`onReconnect`, `connect()` re-issues active `attach` requests after the replacement\nlink is live and then calls each attachment's `onReattach` callback.\n\nReplicas use `onReattach` for live models, logs, and jobs to force a fresh\nsnapshot after reattach. Direct client-handle consumers can use the same callback when\nthey need to reseed UI state after reconnect.\n\nAn attachment signal owns establishment for only that caller. Aborting it before readiness cancels\nthe pending attach when no other caller still needs the topic, while shared callers remain attached.\nAfter `attach()` resolves, the returned unsubscribe owns the established interest.\n\n`dispose()` rejects pending calls, releases live attachments and blob channels, and\nunsubscribes the connection from transport events. It intentionally does not call\n`transport.close()`. This allows a short-lived application handshake connection to\nvalidate a candidate transport before handing that same transport to a stable\nreconnecting connection.\n\nThe protocol layer intentionally has no version handshake. Receivers validate the\nmessage `kind` and required fields in `isWireMessage()`; unknown message kinds are\nignored by transport adapters that parse untrusted frames.\n\n## Typed Clients\n\n`client(contract, connection)` returns a `ContractClient` with the same nested\nshape as the contract:\n\n```ts\nconst contractClient = client(notesApi, connection);\n\nconst sessions = createLiveModelReplicaCache(notesApi.session, contractClient.session, {\n  onChange: {\n    notes: (state, meta) => {\n      console.log('notes model:', state, meta.kind);\n    },\n  },\n});\nconst lease = sessions.acquire({ sessionId: 'demo' });\nconst session = await lease.ready();\n\nconst added = await session.mutations.addNote({ text: 'Typed client mutation' });\nawait added.settled;\nawait lease.release();\nawait sessions.dispose();\n```\n\nLive model and live log accessors are client handles. Use `state(key, name)` or\n`handle(key)` to snapshot/attach without local state, or pass the client handle\nto a replica wrapper.\n\nMutations return `ContractMutationInvocation`:\n\n```ts\ntype ContractMutationInvocation<D, E> = {\n  result: LiveMutationResult<D, E>;\n  settled: Promise<void>;\n};\n```\n\n`MutationCallOptions` lets callers provide a `mutationId`, a per-call\n`timeoutMs` deadline override, and an explicit opt-in retry policy. There is no\nautomatic retry; retries reuse the mutation id, so opted-in retries stay\nidempotent:\n\n```ts\nawait session.mutations.addNote({ text: 'Optimistic title' }, {\n  mutationId: 'custom-mutation',\n  retry: { schedule: retrySchedule({ delaysMs: [250, 1_000], maxRetries: 2 }) },\n});\n```\n\n## Cancellation\n\nWire supports cooperative cancellation for request messages. The client sends a\nprotocol message for the request id:\n\n```ts\n{ kind: 'cancel', id: callId }\n```\n\nTyped procedure clients accept an optional `{ signal }` argument:\n\n```ts\nconst abort = new AbortController();\nconst result = client.slowOperation({ id: 'task' }, { signal: abort.signal });\n\nabort.abort();\nawait result; // rejects with WireError code CANCELLED\n```\n\nIf the signal is already aborted, the call rejects locally without posting.\nServer procedure handlers receive the same signal through `CallMeta`:\n\n```ts\nconst controller = createController(api, {\n  slowOperation: async (input, meta) => {\n    await abortableWork(input, meta.signal);\n    return { ok: true };\n  },\n});\n```\n\nCancellation is cooperative. Long-running handlers should pass the signal into\ntheir own async work, listen for `abort`, or periodically check\n`meta.signal?.aborted`. `snapshot` and `attach` requests also share the same server-side\ncancellation registry. Resourced event-stream activation receives an `AbortSignal` so it can release\npartially-created resources when the final pending attachment disappears.\n\nMutations are intentionally not cancellable through this API; they use\n`mutationId` for idempotency and retry. See [mutations](../live/mutations.md).\nCancelling a `liveJob().start` wire call does not cancel the spawned job; job\ncancellation is domain-level through the generated `<path>.cancel` procedure.\n\n## Composing Controllers\n\nMiddle tiers compose by creating a typed client for the upstream contract and\npassing client handles, local handlers, or replicas into a new controller. This\nkeeps forwarding typed and makes the chosen interception points explicit:\n\nIf the middle tier should only relay the entire upstream contract, use\n`forwardController(contract, client)` instead of spelling out every endpoint:\n\n```ts\nconst upstream = client(workspaceApi, connect(upstreamTransport));\nconst controller = forwardController(workspaceApi, upstream);\n```\n\nUse explicit `createController()` composition when the hop needs local behavior:\n\n```ts\nconst upstreamConnection = connect(upstreamTransport);\nconst upstream = client(workspaceApi, upstreamConnection);\n\nconst conversations = createLiveModelReplicaCache(workspaceApi.conversation, upstream.conversation, {\n  lingerMs: 10 * 60_000,\n});\n\nconst controller = createController(workspaceApi, {\n  // Local override.\n  ping: async () => 'desktop-main',\n\n  // Stateless forwarding.\n  git: upstream.git,\n\n  // Stateful interception/cache before serving downstream clients.\n  conversation: conversations,\n});\n```\n\nFor nested contracts, object destructuring is enough to assemble the\nimplementation shape:\n\n```ts\nconst controller = createController(appApi, {\n  ...upstream,\n  settings: {\n    ...upstream.settings,\n    save: async (input, meta) => {\n      await auditSettingsChange(input, meta.signal);\n      return upstream.settings.save(input, meta);\n    },\n  },\n});\n```\n\nUse replicas when the middle tier needs to inspect, cache, or transform live\nstate before exposing it downstream. Use client handles directly when the middle\ntier is only forwarding calls and live sources.\n\n## Multi-Window Sessions\n\n`createWireSessionHub(controller)` serves the same controller to multiple\ntransport sessions:\n\n```ts\nconst hub = createWireSessionHub(controller);\nconst pair = memoryTransportPair();\n\nhub.open('window-1', pair.right);\nconst contractClient = client(api, connect(pair.left));\n```\n\nOpening the same session id closes the previous transport. `close(id)` closes\none session and calls `transport.close?.()` after disposing the serve loop.\n`dispose()` is async: it closes all sessions and awaits `controller.dispose?.()`.\n\n`Controller.dispose`, when present, returns `Promise<void>`. Wrappers such as\nvalidation, logging, session hubs, tests, process runtimes, and Electron exposure\npropagate that promise so LiveJobSource servers and resource hosts can finish shutdown\nbefore their owner reports disposed.\n\n## Server-Side Call Helpers\n\nFor the full middleware pattern, including handler middleware versus controller\nmiddleware, see [composable middleware](./middleware.md).\n\n`compose(target, middlewares)` from `@emdash/shared/requests` applies target-first\nmiddleware arrays to handlers or controllers. The first array entry is outermost:\nit sees the request first and settles last.\n\n```ts\nconst loadStats = compose(\n  async (input, meta) => {\n    return await fetchStats(input.repo, { signal: meta.signal });\n  },\n  [\n    withTimeout({ timeoutMs: 20_000 }),\n    withRetry({ schedule, shouldRetry: isTransient }),\n    withTimeout({ timeoutMs: 5_000 }),\n  ]\n);\n\nconst controller = createController(api, { loadStats });\n```\n\nIn this example the outer timeout bounds the complete retry program, while the\ninner timeout bounds each attempt. Handler middleware must preserve the handler's\nshape. Procedure handlers receive `(input, meta)` and middleware should preserve\nall `meta` fields while deriving a replacement `meta.signal` when it needs\ncooperative cancellation.\n\n`deduplicate(options?)` from `@emdash/shared/requests` wraps procedure\nimplementations to share one in-flight promise for identical inputs:\n\n```ts\nconst controller = createController(api, {\n  expensiveStats: compose(\n    async (input, meta) => {\n      return await loadStats(input.repo, input.branch, { signal: meta.signal });\n    },\n    [deduplicate({ key: (input) => `${input.repo}:${input.branch}` })]\n  ),\n});\n```\n\nBehavior:\n\n- Default key is `stableStringify(input)`, so object property order does not\n  matter.\n- Only in-flight calls are deduplicated. Settled calls are not cached.\n- Rejections are not cached.\n- `meta.signal` is not part of the key and shared execution is not aborted by\n  one caller.\n- Do not wrap mutations; mutation idempotency is handled by `mutationId`.\n\nSee [../../examples/dedupe/server.ts](../../examples/dedupe/server.ts).\n","isInternal":false,"tokens":2776,"sizeBytes":12707},{"name":"transports.md","path":"packages/wire/docs/api/transports.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/packages/wire/docs/api/transports.md","title":"Api Skill","category":"anthropic-skill","format":"markdown","content":"# Transports\n\nA `WireTransport` is the protocol boundary:\n\n```ts\ntype WireTransport = {\n  post(message: WireMessage): void;\n  onMessage(cb: (message: WireMessage) => void): Unsubscribe;\n  onDisconnect(cb: () => void): Unsubscribe;\n  onReconnect?(cb: () => void): Unsubscribe;\n  onTerminalFailure?(cb: (error: unknown) => void): Unsubscribe;\n  close?(): void;\n};\n```\n\nThe same `serve()`, `connect()`, and `client()` code works across every\ntransport. Only construction changes.\n\n`onReconnect` is optional and fires whenever the transport establishes\nconnectivity, including the first time. Its presence marks the transport as\nreconnect-capable: `connect()` then holds calls issued while disconnected until\nthe next reconnect or the call deadline, and re-attaches live topics after each\nreconnect; replicas then force a fresh snapshot. Transports without it fail\ncalls immediately while disconnected. `onTerminalFailure` fires once when the\ntransport gives up permanently; `connect()` rejects all held and future calls\nwith a `WireError('DISCONNECTED')` carrying the cause. `close()` releases\nlisteners registered by the adapter. It closes the underlying channel only when\nthe adapter owns a closeable channel, such as a `MessagePort`.\n\n## Memory\n\n`memoryTransportPair()` creates paired in-process transports for tests and\nexamples:\n\n```ts\nconst pair = memoryTransportPair();\nserve(pair.right, controller);\n\nconst contractClient = client(api, connect(pair.left));\n```\n\n`pair.disconnect()` disconnects both sides. Each endpoint also exposes `close()`,\nwhich aliases the pair disconnect for test cleanup.\n\n`left` and `right` are the two ends of one duplex channel, not semantic roles.\nPosting on `left` delivers to listeners registered on `right`, and posting on\n`right` delivers to listeners registered on `left`. This mirrors\n`MessageChannel`'s `port1`/`port2`: tests commonly call `serve(pair.right, ...)`\nand `connect(pair.left)`, but the opposite assignment is equally valid.\n\n## DOM MessagePort\n\n`domPortTransport(port)` adapts browser `MessagePort` objects:\n\n```ts\nconst channel = new MessageChannel();\nserve(domPortTransport(channel.port1), controller);\n\nconst connection = connect(domPortTransport(channel.port2));\n```\n\nThe adapter calls `port.start?.()` and listens for `message` and `close` events.\n`close()` removes those listeners and calls `port.close?.()`.\n\n## Electron Windows\n\n`exposeWireToWindows()` serves one controller to many Electron renderer windows\nusing `MessageChannelMain`-style ports:\n\n```ts\nconst stop = exposeWireToWindows(\n  {\n    ipcMain,\n    createMessageChannel: () => new MessageChannelMain(),\n  },\n  controller,\n  { channel: 'wire' }\n);\n```\n\nThe renderer asks for a port, then waits for the browser-side transfer:\n\n```ts\nawait requestWirePort({ ipcRenderer, window }, { channel: 'wire' });\nconst port = await awaitWirePort(window, { channel: 'wire' });\nconst contractClient = client(api, connect(domPortTransport(port as MessagePort)));\n```\n\nOpening a new port for the same `webContents.id` closes the old one. Naturally\nclosed ports are removed from the session map. Internally, the helper uses\n`createWireSessionHub(controller)`, so session teardown also closes the transport.\n\n## Node Streams\n\n`streamTransport(input, output)` frames messages as newline-delimited JSON. It is\nuseful for subprocess, stdio, and SSH-style boundaries:\n\n```ts\nconst transport = streamTransport(child.stdout, child.stdin);\nconst contractClient = client(api, connect(transport));\n```\n\nMalformed frames are ignored. `close`, `end`, and `error` on the readable side\ntrigger disconnect listeners. `close()` stops parsing and clears local listeners;\nit does not close the readable or writable streams because those streams are owned\nby the caller.\n\n## Reconnecting\n\n`reconnectingTransport(connectOnce, options?)` wraps an async transport factory:\n\n```ts\nconst transport = reconnectingTransport(\n  async () => {\n    const pair = await openRemoteWirePair();\n    return pair.left;\n  },\n  { backoffMs: [100, 250, 500, 1000] }\n);\n\nawait transport.ready();\n```\n\n`connectOnce` is the readiness boundary for one physical connection. The returned\ntransport remains private until the promise resolves. Applications that require a\nhandshake should open the candidate, run the handshake against it, dispose the\ntemporary logical `Connection`, and only then return the candidate:\n\n```ts\nconst transport = reconnectingTransport(\n  async () => {\n    const candidate = await openRemoteTransport();\n    const handshakeConnection = connect(candidate);\n    const handshakeClient = client(api, handshakeConnection);\n    try {\n      const result = await handshakeClient.initialize({ version: PROTOCOL_VERSION });\n      if (!result.success) throw new ProtocolMismatchError(result.error);\n      return candidate;\n    } catch (error) {\n      candidate.close?.();\n      throw error;\n    } finally {\n      handshakeConnection.dispose();\n    }\n  },\n  {\n    shouldRetry: (error) => !(error instanceof ProtocolMismatchError),\n  }\n);\n\nawait transport.ready();\nconst stableClient = client(api, connect(transport));\n```\n\nThis ordering guarantees that held calls and live-topic reattachments cannot\novertake an application handshake. Wire itself remains version-agnostic; the\napplication owns the handshake contract and permanent-error classification.\n\nThe reconnecting transport does not queue messages. `post()` while no ready\ninner transport exists throws a `WireError('DISCONNECTED')`; robustness is\nowned entirely by `connect()`, which holds calls issued while disconnected\nuntil the next reconnect or the call deadline. When an inner transport\ndisconnects, listeners are notified, readiness resets, and reconnection starts.\n\n`ready()` resolves after the current `connectOnce` succeeds and its candidate is\ninstalled. After a disconnect, a new call to `ready()` waits for the replacement\ngeneration. A previously resolved readiness promise does not become pending\nagain, so disconnect observers should call `ready()` after receiving the\ndisconnect event.\n\n`shouldRetry(error, { attempt, isReconnect })` classifies factory or handshake\nfailures. Returning `false`, or exhausting a finite `retrySchedule`, permanently\nstops that transport, rejects its current readiness promise, and fires\n`onTerminalFailure` with the terminal error. Future `post()` calls fail with the\nterminal error. By default, failures remain retryable under the configured\nschedule.\n\n`onReconnect` fires whenever an inner transport reaches readiness, including\nthe first one. `Connection` listens for that signal to flush held calls and\nre-issue active `attach` requests; replicas refresh their snapshots after\nreattach. `close()` stops the retry loop, closes the current inner transport if\npresent, fires `onTerminalFailure`, and clears listeners.\n\n## Composition Notes\n\n`reconnectingTransport()` accepts `clock` and `retrySchedule` options. The legacy\n`backoffMs` array is normalized to a repeat-last retry schedule. Closing the\ntransport synchronously aborts pending reconnect sleeps and disposes late\nconnections.\n\nElectron window exposure returns an async cleanup function. Await it when tearing\ndown a runtime host so session hubs and controllers finish disposal before the\nworker or window bridge is replaced.\n\nTransport composition is ordinary function wrapping, so order matters when\nstacking adapters around `reconnectingTransport()`. For semantic telemetry (one\nevent per logical call, snapshot, attachment, cancellation, resync, or mutation\ndedupe), attach a `WireInstrumentation`; see\n[observability](../observability.md).\n","isInternal":false,"tokens":1627,"sizeBytes":7587},{"name":"event-stream.md","path":"packages/wire/docs/live/event-stream.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/packages/wire/docs/live/event-stream.md","title":"Live Skill","category":"anthropic-skill","format":"markdown","content":"# Event Streams\n\n`eventStream({ key, event })` exposes a keyed server-to-client event channel for\nloss-tolerant notifications. It reuses the wire live attachment protocol but does\nnot retain events or expose meaningful snapshots.\n\nUse event streams when subscribers can recover from missed events by resyncing\nfrom another source of truth, such as a file tree, cache, or database query. Use\n`liveLog` for retained append-only text and `liveModel` for convergent state.\n\n## Contract\n\n```ts\nconst api = defineContract({\n  fileEvents: eventStream({\n    key: z.object({ rootPath: z.string() }),\n    event: z.object({\n      kind: z.enum(['create', 'update', 'delete']),\n      path: z.string(),\n    }),\n  }),\n});\n```\n\nThe key addresses an event stream instance. The event schema types the\nclient-side `onEvent` callback and documents the payload shape.\n\n## Server\n\nMost servers use `createEventStreamHost()`:\n\n```ts\nconst fileEvents = createEventStreamHost(api.fileEvents);\n\nconst controller = createController(api, {\n  fileEvents,\n});\n\nfileEvents.emit(\n  { rootPath: '/repo' },\n  { kind: 'update', path: '/repo/package.json' }\n);\n```\n\n`emit()` is fire-and-forget. If no client is attached to that key, the event is\ndropped. When the last subscriber detaches, the host discards the keyed source.\n\nIf attachment presence owns an external resource, pass lifecycle hooks to the\nhost. `onActive` runs when the first subscriber attaches to a key, and `onIdle`\nruns when the last subscriber detaches:\n\n```ts\nconst resourceEvents = resourcedStream({\n  key: z.object({ rootPath: z.string() }),\n  event: z.object({ kind: z.string(), path: z.string() }),\n});\nconst fileEvents = createEventStreamHost(resourceEvents, {\n  onActive(key) {\n    void startWatcher(key);\n  },\n  onIdle(key) {\n    void stopWatcher(key);\n  },\n});\n```\n\nThis is useful for resources that should be recreated after reconnect. The wire\nclient automatically reattaches live topics after reconnect, so `onActive` runs\nagain in the new server process.\n\nWhen a successful attach must mean that the resource is ready, define the endpoint with\n`resourcedStream()` and provide `activate`. Activation receives a signal and resolves to its owned\ndisposer:\n\n```ts\nconst fileEvents = createEventStreamHost(api.fileEvents, {\n  async activate(key, signal) {\n    const watcher = await startWatcher(key, { signal });\n    return () => watcher.dispose();\n  },\n});\n```\n\nConcurrent subscribers share one activation. The host aborts pending activation when the final\nsubscriber leaves, disposes a late result, and keeps activation alive when another subscriber still\nowns the key.\n\nYou can also pass a resolver to `createController()` if another component owns\nthe source:\n\n```ts\ncreateController(api, {\n  fileEvents: (key) => sources.get(key.rootPath),\n});\n```\n\n## Client\n\n```ts\nconst unsubscribe = await client.fileEvents.subscribe(\n  { rootPath: '/repo' },\n  {\n    onEvent(event) {\n      console.log(event.kind, event.path);\n    },\n    onGap() {\n      void reloadFileTree();\n    },\n    onError(error, { retrying }) {\n      if (!retrying) showFileEventStreamError(error.message);\n    },\n    signal,\n  }\n);\n```\n\n`onGap` runs after the underlying transport reattaches. Events may have been lost\nwhile the link was disconnected or while a supervised subprocess restarted, so\nconsumers should refresh any derived state in that callback.\n\nInitial attachment does not call `onGap`; subscribers should perform their own\ninitial load before or after subscribing.\n\nThe `subscribe()` promise rejects if the initial attachment fails. After an\nattachment is established, `DISCONNECTED` reattach failures call `onError` with\n`retrying: true` and remain registered for a later reconnect. Non-retryable\nreattach errors call `onError` with `retrying: false` and terminate the\nattachment.\n\n## Semantics\n\n- Events are at-most-once and only delivered to currently attached clients.\n- No historical events are retained for late subscribers.\n- Event stream keys are validated by controller validation.\n- Event payloads are not validated on the push path, matching other live updates.\n- Forwarding preserves upstream gaps and reattach errors even when the downstream\n  transport remains connected.\n\n## Example\n\nSee [../../examples/event-stream/client.ts](../../examples/event-stream/client.ts)\nfor a runnable in-process example.\n","isInternal":false,"tokens":975,"sizeBytes":4343},{"name":"live-job.md","path":"packages/wire/docs/live/live-job.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/packages/wire/docs/live/live-job.md","title":"Live Skill","category":"anthropic-skill","format":"markdown","content":"# Live Jobs\n\nLive jobs model async work as a live state machine. They are useful when callers\nneed both a final result and a stream of progress updates.\n\n## State Shape\n\n`liveJobStateSchema(progress, result, error)` creates a schema for:\n\n```ts\ntype LiveJobState<P, R, E> =\n  | { status: 'running'; startedAt: number; progress: P[]; progressCount: number }\n  | { status: 'succeeded'; startedAt: number; finishedAt: number; progress: P[]; result: R }\n  | { status: 'failed'; startedAt: number; finishedAt: number; progress: P[]; error?: E; cause?: SerializedError }\n  | { status: 'cancelled'; startedAt: number; finishedAt: number; progress: P[] };\n```\n\n`progress` is a retained ring of recent entries. `progressCount` is the total\nnumber of progress events emitted, including entries that may have rolled out of\nthe retained ring. Terminal states retain lifecycle timestamps and the most\nrecent progress entries, but they do not carry `progressCount`.\n\n## Server\n\n`LiveJobSource<I, P, R, E>` accepts a handler that returns `Result<R, E>`:\n\n```ts\nconst jobs = new LiveJobSource<Input, Progress, Result, ErrorState>(\n  async (input, ctx) => {\n    ctx.progress({ step: 'checkout' });\n    await build(input, ctx.signal);\n    ctx.progress({ step: 'package' });\n    return ok({ artifact: `${input.target}.zip` });\n  },\n  { toError: (error) => ({ message: error instanceof Error ? error.message : String(error) }) }\n);\n\nconst { jobId } = jobs.start({ target: 'desktop' });\n```\n\nOptions:\n\n- `generation`: optional fixed generation for every job state model.\n- `terminalRetainMs`: how long terminal state remains attachable. Default:\n  `LIVE_JOB_TERMINAL_RETAIN_MS` (5 minutes).\n- `idFactory`: custom job id factory, useful for deterministic tests.\n- `clock`: custom timestamp source, useful for deterministic tests.\n- `toError`: optional fallback for unexpected thrown errors. Domain failures\n  should be returned as `err(error)` from the handler.\n- `onRunStarted`, `onRunChanged`, and `onRunEvicted`: optional hooks for callers\n  that want to maintain their own job listing.\n\nHandlers receive `LiveJobContext`:\n\n- `ctx.progress(value)` appends retained progress and emits an update.\n- `ctx.signal` is an `AbortSignal`. If the signal is aborted, the job finishes\n  as `cancelled`.\n\nReturn `ok(result)` for successful completion and `err(error)` for expected\ndomain failures. If the handler throws and `toError` is configured, the thrown\nvalue is mapped into typed `error`; otherwise the failed state carries a\nserialized `cause`.\n\n`source(jobId)` returns a read-only live source for that job. `snapshot(jobId)` and\n`getState(jobId)` are convenience accessors. `cancel(id)` aborts a running job.\n`dispose()` is async: it aborts running jobs, waits for cooperative handlers to\nsettle, clears eviction timers, and removes all job state.\n\nTerminal job state is retained for `LIVE_JOB_TERMINAL_RETAIN_MS` (5 minutes) so\nlate clients can reattach by id. This retention is process-local, not durable.\n\n## Consumers\n\nConsumers normally reach jobs through the API layer. The contract client starts,\ncancels, and attaches to jobs without retaining local state:\n\n```ts\nconst { jobId } = await contractClient.build.start({ target: 'desktop' });\nconst handle = contractClient.build.handle(jobId);\n\nconst detach = await handle.attach((update) => {\n  console.log(update.delta);\n});\n\nawait contractClient.build.cancel(jobId);\ndetach();\n```\n\nUse `createLiveJobReplicaCache()` when a consumer wants a `result` promise, progress\ncallbacks, ref-counted attachment, and terminal-state retention. If the job fails,\n`ReplicaJob.result` rejects with `LiveJobFailedError`; if it is cancelled, it\nrejects with `LiveJobCancelledError`. Progress emitted by a seed is suppressed so\nreattaching clients do not replay old progress as fresh events.\n\n## Contract Endpoint\n\nThe API contract layer can expose a job directly:\n\n```ts\nconst api = defineContract({\n  build: liveJob({\n    input: z.object({ target: z.string() }),\n    progress: z.object({ step: z.string() }),\n    result: z.object({ artifact: z.string() }),\n    error: z.object({ message: z.string() }),\n  }),\n});\n```\n\nOn the server, implement the endpoint with `{ run, toError? }`:\n\n```ts\nconst controller = createController(api, {\n  build: {\n    run: async (input, ctx) => {\n      ctx.progress({ step: 'compile' });\n      return ok({ artifact: `${input.target}.zip` });\n    },\n    toError: (error) => ({\n      message: error instanceof Error ? error.message : String(error),\n    }),\n  },\n});\n```\n\nThe typed client exposes a live job client handle. Use it directly for\nlow-level forwarding, or wrap it in `createLiveJobReplicaCache()` when a consumer wants\nref-counted local state:\n\n```ts\nconst jobs = createLiveJobReplicaCache(api.build, contractClient.build, { lingerMs: 30_000 });\nconst lease = await jobs.start({ target: 'desktop' });\nconst handle = await lease.ready();\n\nhandle.onProgress((progress) => console.log(progress.step));\nconsole.log(await handle.result);\n\nawait lease.release();\n\nconst reattachedLease = jobs.acquire(handle.jobId);\nconst reattached = await reattachedLease.ready();\nconsole.log(await reattached.result);\nawait reattachedLease.release();\nawait jobs.dispose();\n```\n\n`ReplicaJob` exposes `jobId`, `ready`, `result`, `getState()`,\n`onProgress(cb)`, and `cancel()`. The `LiveJobReplicaCache` manager owns ref counting,\nsubscription sharing, and terminal-state retention.\n\n`LiveJobSource` accepts a `Clock` for deterministic start/finish timestamps and\nterminal retention eviction. Server disposal is awaitable: it cancels active job\nruns, waits for cooperative handlers to settle, and disposes retention timers\nbefore returning.\n\nCancellation at the procedure/wire level is documented in\n[serving](../api/serving.md#cancellation). Job cancellation is domain-level:\n`ReplicaJob.cancel()` calls the generated `<path>.cancel` procedure, which calls\n`LiveJobSource.cancel(jobId)`.\n\nSee [../../examples/job-contract/client.ts](../../examples/job-contract/client.ts).\n","isInternal":false,"tokens":1417,"sizeBytes":6008},{"name":"live-log.md","path":"packages/wire/docs/live/live-log.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/packages/wire/docs/live/live-log.md","title":"Live Skill","category":"anthropic-skill","format":"markdown","content":"# Live Logs\n\nLive logs are append-only text streams with retained tail snapshots. They are a\ngood fit for terminal output, build logs, and agent process output where clients\nneed to attach late and still see recent context.\n\n## Server\n\n`LiveLogSource` stores a bounded text buffer. `append(chunk)` emits\n`{ chunk }` deltas to subscribers and updates the retained snapshot:\n\n```ts\nconst server = new LiveLogSource({ generation: 3000, maxBufferBytes: 12 });\n\nexport function appendLine(line: string): void {\n  server.append(`${line}\\n`);\n}\n```\n\nOptions:\n\n- `generation`: optional fixed generation, useful in tests.\n- `maxBufferBytes`: retained buffer size. The default is 1 MiB.\n\nSnapshots contain `LiveLogSnapshotData`:\n\n```ts\ntype LiveLogSnapshotData = {\n  baseOffset: number;\n  text: string;\n  truncated: boolean;\n};\n```\n\n`baseOffset` is the byte offset of `text` in the logical full log. When the\nserver drops old bytes because `maxBufferBytes` was exceeded, `truncated` is\n`true`. The server keeps at least the newest chunk even if it exceeds the byte\nlimit.\n\n`reseed()` starts a new generation and resets sequence to `0`. With no argument,\nit clears retained text and resets `baseOffset` to `0`; with retained snapshot\ndata, it seeds the buffer from that data.\n\n## Consumers\n\nConsumers normally reach logs through the API layer. The client handle is useful\nwhen you want to stream directly into a terminal or log viewer:\n\n```ts\nconst output = contractClient.activity.handle(session);\nconst snapshot = await output.snapshot();\nterm.write(snapshot.data.text);\n\nconst detach = await output.attach((update) => {\n  term.write((update.delta as { chunk: string }).chunk);\n});\n```\n\nUse `createLiveLogReplicaCache()` when a process wants a retained local buffer that can\nalso be served downstream. Without a custom sink, the replica keeps an eager\nbounded `LiveLogSource` buffer and `text()` remains readable. With a custom `LogSink`,\nthe sink owns storage; readable `LogStore`s add `text()`, while write-only sinks\ncan stream directly into xterm without a duplicate text buffer.\n\nThe replica's internal log follower tracks the followed generation/sequence\nplus the UTF-8 byte offset already written to its sink. On reconnect or explicit `refresh()`, it\nsplices from the retained snapshot when the generation still matches, appending\nonly missing bytes instead of resetting rendered output. Generation changes or\nevicted gaps still reset the sink from the retained snapshot.\n\nThe underlying protocol follower resyncs on update-before-seed, generation\nmismatch, sequence gap, or invalid log delta. Resync events use the same `resync`\ninstrumentation hook as live models.\n\n## API Layer Usage\n\nContracts expose logs with `liveLog({ key })`:\n\n```ts\nconst api = defineContract({\n  activity: liveLog({ key: sessionKeySchema }),\n});\n\nconst controller = createController(api, {\n  activity: () => activityLogServer,\n});\n\nconst activity = contractClient.activity.handle(session);\nconst detach = await activity.attach((update) => {\n  console.log((update.delta as { chunk: string }).chunk);\n});\n\ndetach();\n```\n\nThe API layer handles topic encoding, snapshots, attachment, and detachment.\n\nSee [../../examples/live-log/client.ts](../../examples/live-log/client.ts).\n","isInternal":false,"tokens":721,"sizeBytes":3248},{"name":"live-model.md","path":"packages/wire/docs/live/live-model.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/packages/wire/docs/live/live-model.md","title":"Live Skill","category":"anthropic-skill","format":"markdown","content":"# Live States and Protocol\n\nLive states are reactive JSON state containers. A server owns authoritative\nstate, emits ordered Immer patches, and clients apply those patches locally.\nWhen a client detects a gap, it refetches a snapshot and resumes from the new\ngeneration.\n\n## Protocol Terms\n\nThe shared protocol lives in `src/live/protocol`.\n\n- `LiveSnapshot<T>` is the full state at a cursor:\n  `{ generation, sequence, timestamp, data }`.\n- `LiveUpdate` is an ordered delta:\n  `{ generation, baseSequence, sequence, timestamp, delta, mutationIds? }`.\n- `LiveCursor` is `{ generation, sequence }` and identifies a point in a live\n  state stream.\n- `LiveSource` is the common server-side shape consumed by the API layer:\n  `snapshot()` plus `subscribe(cb)`.\n\n`generation` changes when the server calls `reseed()`. `sequence` increments for\neach effective patch in a generation. `baseSequence` must equal the client's\ncurrent sequence; otherwise the client has missed an update and resyncs from\n`snapshot()`.\n\n## Server\n\n`LiveStateSource<T>` owns one authoritative state object. Mutate it with\n`produce()`, not by mutating the original object:\n\n```ts\nconst server = new LiveStateSource<TaskListState>(\n  {\n    tasks: [{ id: 'task-1', title: 'Read the plan', done: false }],\n    filter: 'all',\n  },\n  1000\n);\n\nconst cursor = server.produce(\n  (draft) => {\n    draft.tasks.push({ id: 'task-2', title: 'Apply the first patch', done: false });\n  },\n  { mutationIds: ['example-add-task'] }\n);\n```\n\n`produce()` returns the cursor containing the change. If the mutator is a no-op,\nno update is emitted and the current cursor is returned. `snapshot()` deep\nclones the current state. `reseed(next?)` replaces the generation, resets\nsequence to `0`, optionally replaces state, and forces clients to resync on the\nnext observed update.\n\n## Consumers\n\nConsumers normally reach live states through a live model contract. Use the\ncontract client when you only need protocol access:\n\n```ts\nconst model = contractClient.conversation.state({ sessionId }, 'state');\nconst snapshot = await model.snapshot();\nconst detach = await model.attach((update) => {\n  console.log(update.delta);\n});\n```\n\nWrap that live state client handle in `createLiveModelReplicaCache()` when a process wants local\nstate, mutation settling, ref counting, or a downstream `LiveSource`; see\n[replicas](./replicas.md#model-replicas).\n\nThe package also has low-level protocol followers internally. They consume\nsnapshots and ordered updates, then resync when:\n\n- an update arrives before `seed()`.\n- `generation` differs from the local generation.\n- `baseSequence` differs from the local sequence.\n- Immer patch application throws.\n- schema validation fails in non-production builds.\n\nThe reported resync reasons are `generation`, `sequence-gap`, `patch-failed`,\nand `validation`. Schema validation is skipped when `NODE_ENV === 'production'`;\nthe generation and sequence checks are the primary correctness mechanism.\n\nEvery follower is constructed with a required resync failure policy that\ndecides what happens when the snapshot refetch itself fails. Wire ships two\ncanned policies: `resyncRetry({ schedule? })` retries on a bounded backoff\nuntil success or dispose, and `resyncMarkStale()` gives up while keeping\nstaleness observable (`stale` on the client) until the next update or reattach\ntriggers a fresh attempt. Production replicas use `resyncRetry`. Every failure\nfires the `resyncFailed` instrumentation event. `refresh()` resolves only when\nthe follower is fresh: concurrent calls coalesce onto the in-flight resync and\na trigger arriving mid-reseed re-runs the loop before resolution. Disposing a\nfollower-based client rejects all of its pending waiters — including\nwait-forever waiters (`timeoutMs <= 0`) — with a disposed error.\n\n## Cursor and Mutation Waiters\n\n`waitForCursor(cursor, timeoutMs?)` resolves when the client has reached a\ncursor. `waitForMutation(mutationId, timeoutMs?)` resolves when an applied update\ncontains the mutation id, or when a seed/resync lands because the snapshot is\nauthoritative.\n\nThese waiters are used by mutation settling; see [mutations](./mutations.md).\n\nSee [../../examples/state-kernel/client.ts](../../examples/state-kernel/client.ts)\nfor an end-to-end example.\n","isInternal":false,"tokens":973,"sizeBytes":4272},{"name":"mutations.md","path":"packages/wire/docs/live/mutations.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/packages/wire/docs/live/mutations.md","title":"Live Skill","category":"anthropic-skill","format":"markdown","content":"# Mutations\n\nMutations connect API calls to live model updates. A mutation can update one\nmodel, many instances of one model, or several model refs. The key extra piece\nis the `mutationId`: it tags emitted `LiveUpdate`s so clients can prove their\nbound live models have observed the mutation.\n\n## Why Mutation IDs Exist\n\nAn RPC result only tells the caller that the server handler finished. It does\nnot prove every live subscription in the UI has applied the corresponding\npatches. Mutation ids bridge that gap:\n\n```ts\nserver.produce(\n  (draft) => {\n    draft.tasks.push({ id: 'task-2', title: 'Apply the first patch', done: false });\n  },\n  { mutationIds: ['example-add-task'] }\n);\n```\n\nThe update carries `mutationIds: ['example-add-task']`. A replica can resolve\n`waitForMutation('example-add-task')` when its local model applies that update.\n\n## Providers and Context\n\nLive model contract mutations run against a `LiveModelProvider` — most commonly\none produced by `expose()` from `@emdash/wire/state`, which bridges kernel state\nonto the endpoint. Mutation handlers update kernel cells and await the observed\nrevision so the returned cursor is settled:\n\n```ts\nconst sessions = expose(api.session, { metadata: (key) => metadataCell(key) }, {\n  mutations: {\n    async setTitle(ctx) {\n      const revision = metadataCell(ctx.key).update(\n        (previous) => ({ ...previous, title: ctx.input.title }),\n        { mutationIds: [ctx.mutationId] }\n      );\n      await ctx.observed('metadata', revision);\n      return ok({ title: ctx.input.title });\n    },\n  },\n});\n```\n\nKeys use `stableStringify()`, so object key order does not matter when providers\nand client bindings look up an instance. The mutation context is instance-bound:\n`ctx.key` identifies the addressed member and `ctx.observed(name, revision)`\nrecords the cursor of every touched member state. The wire result is:\n\n```ts\ntype LiveMutationResult<D, E> =\n  | { success: true; data: { data: D; cursors: LiveCursorEntry[] } }\n  | { success: false; error: E };\n```\n\nThe `data.data` value is the domain result. `data.cursors` tells the client which\nlive model bindings need to catch up.\n\n## Client Settling\n\n`LiveModelReplicaCache.acquire(key)` returns a `ReplicaInstance` for one group key. Its\nmutation methods call the live model client handle and then settle against the local\n`ReplicaState`s.\n\nGroup mutation methods return `{ result, settled }`:\n\n```ts\nconst sessions = createLiveModelReplicaCache(api.session, contractClient.session);\nconst lease = sessions.acquire({ sessionId: 'demo' });\nconst session = await lease.ready();\n\nconst added = await session.mutations.addNote({ text: 'Typed client mutation' });\nawait added.settled;\nawait lease.release();\nawait sessions.dispose();\n```\n\n`settled` waits for every cursor in the mutation result. For each cursor entry,\nit resolves when either:\n\n- the matching binding applies an update tagged with the mutation id, or\n- the matching binding reaches the returned cursor.\n\nThis lets UI code safely read live client snapshots after `await settled`.\n\n## Group Contract Mutations\n\nThe API layer integrates mutations through `liveModel()` member\nmutations. Each group mutation becomes a client method:\n\n```ts\nconst updated = await session.mutations.setTitle({ title: 'Grouped wire' }, {\n  mutationId: 'custom-mutation',\n});\nawait updated.settled;\n```\n\nIf no id is provided, the replica mutation helper generates one with\n`createMutationId()`.\nExplicit ids are useful for optimistic previews, where the preview and server\nmutation must share the same confirmation id.\n\n## Idempotency and Retries\n\nThe server-side idempotency cache used by `expose()` stores settled mutation\nresults by `mutationId` and shares one in-flight execution for concurrent\nduplicates.\n\nBy default the cache keeps entries for 5 minutes with a 1000-entry cap.\nConfigure or disable it through the `idempotency` option:\n\n```ts\nconst sessions = expose(api.session, states, {\n  mutations,\n  idempotency: { ttlMs: 60_000, maxEntries: 500 },\n});\n\nconst withoutDedupe = expose(api.session, states, { mutations, idempotency: false });\n```\n\nThe client never retries mutations automatically. Callers opt in per call with\nan explicit retry schedule; retries reuse the same `mutationId`, so the\nserver-side cache deduplicates them:\n\n```ts\nawait session.addNote(input, {\n  mutationId: 'add-note-1',\n  retry: { schedule: retrySchedule({ delaysMs: [250, 1_000], maxRetries: 2 }) },\n});\n```\n\nOpted-in retries fire only for `DISCONNECTED` and `TIMEOUT` errors; they never\nhappen for `CANCELLED` errors.\n\nThe cache is process-local and temporary. It provides at-most-once behavior\nwithin one server process lifetime, not durable exactly-once semantics. If a\nmutation has durable side effects such as database writes, store the\n`mutationId` in that domain layer too.\n\nUse `procedure()` for API calls that do not need live model cursor settling.\n`mutation()` is only valid as a member of `liveModel().mutations`\nin the contract API.\n\n","isInternal":false,"tokens":1155,"sizeBytes":4994},{"name":"replicas.md","path":"packages/wire/docs/live/replicas.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/packages/wire/docs/live/replicas.md","title":"Live Skill","category":"anthropic-skill","format":"markdown","content":"# Replicas\n\nReplicas are consumer-instantiated materialization wrappers around client handles from\n`client()`. The typed client itself never stores live state. A consumer chooses\none of four shapes:\n\n- Own state: `LiveStateSource`, `LiveLogSource`, `LiveJobSource`, or an\n  `expose()` provider over kernel state.\n- Stream directly: use a client `snapshot()`/`attach()` handle with no local store,\n  for example PTY output written straight into xterm.\n- Forward: pass client handles or subtrees to `createController()` so a hop stays stateless.\n- Replica: wrap a client handle to hold local state, share upstream subscriptions, and\n  serve downstream clients.\n\nEvery replica manager has the same lifecycle shape:\n\n```ts\nconst lease = replica.acquire(key);\nconst instance = await lease.ready();\nawait lease.release();\nawait replica.dispose();\n```\n\n`acquire()` increments the ref count for that key. The first lease opens the\nupstream subscription, concurrent leases share it, and the last release starts\nthe optional `lingerMs` timer. `peek(key)` returns a warm instance while it is\nretained.\n\n## Model Replicas\n\nLive models are exposed only through `liveModel()`. A\n`LiveModelReplicaCache` follows a live model client handle and yields a `ReplicaInstance`:\n\n```ts\nimport { createImmutableMobxStore } from '@emdash/wire/mobx';\n\nconst conversations = createLiveModelReplicaCache(api.conversation, contractClient.conversation, {\n  lingerMs: 30_000,\n  stores: {\n    state: () => createImmutableMobxStore(),\n  },\n  onChange: {\n    state: (value, meta) => console.log(value, meta.kind),\n  },\n});\n\nconst lease = conversations.acquire({ conversationId: 'demo' });\nconst conversation = await lease.ready();\n\nconst updated = await conversation.mutations.setTitle({ title: 'Replicated' });\nawait updated.settled;\nconsole.log(conversation.states.state.current());\n```\n\n`ReplicaInstance.states` contains one `ReplicaState` per contract member.\n`ReplicaState` follows upstream snapshots and updates, stores current state in a\npluggable `StateStore`, and re-emits updates in a replica-local cursor space. If\nno store is supplied, the replica uses the plain immutable store.\nMobX consumers can pass stores per liveState through `stores`. Use\n`createImmutableMobxStore()` for snapshot/reference consumers such as\n`useSyncExternalStore`, or `createReactiveMobxStore()` for `observer` components\nthat benefit from property-level MobX tracking.\n\nMutation helpers return `{ result, settled }`. On success, the replica translates\nupstream cursors to local cursors before returning them to downstream clients, so\nboth local UI and served clients settle against the same local state.\n\nA cursor-translation timeout never fails a committed mutation. If the replica\ncannot observe the mutation's upstream cursor in time, the mutation still\nresolves successfully and the result data carries `settled: false` (client-side\nmetadata; the wire shape of `LiveMutationResult` is unchanged). Instrumentation\nrecords the timeout via `cursorTranslationTimeout`, and the `settled` promise\nstays best-effort as before.\n\n## Log Replicas\n\nUse a `LiveLogReplicaCache` when a process needs a local retained text buffer or wants\nto serve log output downstream:\n\n```ts\nimport { createMobxLogStore } from '@emdash/wire/mobx';\n\nconst outputs = createLiveLogReplicaCache(api.ptyOutput, contractClient.ptyOutput, {\n  lingerMs: 10_000,\n  maxBufferBytes: 1024 * 1024,\n  store: () => createMobxLogStore(),\n});\n\nconst lease = outputs.acquire({ sessionId });\nconst output = await lease.ready();\noutput.onAppend((chunk) => index(chunk));\nconsole.log(output.text());\n```\n\nWithout a custom sink, the log replica keeps an eager internal `LiveLogSource` buffer\nthat is readable through `text()` and can serve downstream clients. With a custom\nsink, the sink owns storage: readable `LogStore`s expose `text()`, while\nwrite-only `LogSink`s (for example xterm scrollback) support reset/append without\nallocating a duplicate string buffer.\n\nFor terminal rendering, prefer the client handle directly:\n\n```ts\nconst output = contractClient.ptyOutput.handle({ sessionId });\nterm.write((await output.snapshot()).data.text);\nconst detach = await output.attach((update) => {\n  term.write((update.delta as { chunk: string }).chunk);\n});\n```\n\n## Job Replicas\n\n`LiveJobReplicaCache` wraps a live job client handle. It forwards `start()` and `cancel()`,\nmaterializes job state by `jobId`, and keeps terminal state readable under lease\nor retention:\n\n```ts\nimport { createImmutableMobxStore } from '@emdash/wire/mobx';\n\nconst jobs = createLiveJobReplicaCache(api.build, contractClient.build, {\n  lingerMs: 30_000,\n  store: () => createImmutableMobxStore(),\n});\n\nconst lease = await jobs.start({ target: 'desktop' });\nconst job = await lease.ready();\njob.onProgress((progress) => console.log(progress.step));\nconsole.log(await job.result);\n\nawait lease.release();\n\nconst late = jobs.acquire(job.jobId);\nconsole.log((await late.ready()).getState());\n```\n\nWhen a job reaches `succeeded`, `failed`, or `cancelled`, the replica detaches\nfrom the upstream live topic but retains the local terminal state while leases or\n`lingerMs` keep it warm.\nThe job replica also supports a pluggable reset/current `JobStore`, so\n`getState()` can be observable. The internal `LiveStateSource` used for downstream\nserving is created only if `snapshot()` or `subscribe()` is requested.\n\n## Serving Replicas\n\nReplicas can be passed directly into `createController()`:\n\n```ts\nconst upstream = client(api, connect(sshTransport));\n\nconst controller = createController(api, {\n  conversation: createLiveModelReplicaCache(api.conversation, upstream.conversation),\n  ptyOutput: upstream.ptyOutput, // forward bytes without buffering\n  build: createLiveJobReplicaCache(api.build, upstream.build),\n});\n```\n\nUse replicas only when the hop needs local state. If the hop is a pure relay,\nforward the client subtree instead.\n","isInternal":false,"tokens":1347,"sizeBytes":5913},{"name":"observability.md","path":"packages/wire/docs/observability.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/packages/wire/docs/observability.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Observability\n\n`@emdash/wire` exposes observability at the same boundaries that own runtime\nbehavior: server calls, client calls, live topic attachment, mutation dedupe,\nlive-client resyncs, transport messages, and scope cleanup.\n\n## Ambient Logger\n\nThe logger interface and redaction utilities live in `@emdash/shared/logger`.\nWire uses the shared ambient logger to attach call context without passing a\nlogger through every domain function:\n\n```ts\nimport { log, runWithLogger } from '@emdash/shared/logger';\n\nawait runWithLogger(logger.child({ requestId: 'r1' }), async () => {\n  log.info('handling request');\n});\n```\n\nNode entry points can install the `AsyncLocalStorage` store:\n\n```ts\nimport { installAsyncLogContext } from '@emdash/shared/logger/node';\n\ninstallAsyncLogContext();\n```\n\nBrowser and renderer code use a synchronous fallback. It still supports a root\nlogger and scoped synchronous blocks, but it does not preserve context across\n`await`.\n\n## Instrumentation Hooks\n\nUse `WireInstrumentation` (exported from `@emdash/wire/rpc`) for typed events\nthat can be adapted to logs, metrics, or tracing. The seam is threaded through\nthe public options of `serve()`, `connect()`, replicas, and `expose()`:\n\n```ts\nimport type { WireInstrumentation } from '@emdash/wire/rpc';\n\nconst instrumentation: WireInstrumentation = {\n  callEnd: (event) => logger.debug('call finished', event),\n  resyncFailed: (event) => logger.warn('resync failed', event),\n};\n\nserve(transport, controller, { logger, instrumentation });\nconst connection = connect(clientTransport, { instrumentation });\nconst contractClient = client(api, connection);\n```\n\nHooks currently cover:\n\n- procedure start/end, cancellation, and duration.\n- snapshot timing and errors.\n- live topic attach/detach.\n- live model/log resync reasons and resync failures.\n- replica cursor-translation timeouts.\n- mutation dedupe hits.\n- scope cleanup errors.\n- transport connect/disconnect events.\n\nThere are no bundled logger adapters: adapt the typed events to your logging or\nmetrics stack at the application edge.\n\nServing and client options are documented in [API serving](./api/serving.md).\nTransport construction is documented in [API transports](./api/transports.md).\n\n## Scope Logging\n\n`createScope({ logger, label })` attaches a logger to the scope tree. Children\ninherit the logger with a `scope` binding containing their label path:\n\n```ts\nconst runtimeScope = createScope({ label: 'runtime', logger });\nconst sessionScope = runtimeScope.child('session:abc');\n\nsessionScope.log.info('session started');\n```\n\nCleanup errors and failed runs are reported through the scope logger by default.\nUse `describeScope(scope)` for a lightweight label tree when debugging retained\nresources or stuck async work. Scope descriptions include active run labels,\nstart times, and cancellation state. When the scope is created with a custom\n`Clock`, those start times come from that clock, which keeps lifecycle diagnostics\ndeterministic in tests.\n\nScope lifecycle behavior is documented in [runtime lifecycle](./runtime/lifecycle.md)\nand [structured concurrency](./runtime/structured-concurrency.md).\n","isInternal":false,"tokens":672,"sizeBytes":3146},{"name":"components.md","path":"packages/wire/docs/runtime/components.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/packages/wire/docs/runtime/components.md","title":"Runtime Skill","category":"anthropic-skill","format":"markdown","content":"# Components\n\n`WireComponent` is the authored abstraction for runtime and service contracts that can be created\nin-process or served from a worker process.\n\nUse a component when a contract has a reusable runtime implementation and the composition root should\nchoose whether it runs in the current process or in a supervised worker. A component is not a\ndependency injection container: it describes one contract implementation, its explicit requirements,\nits config schema, and how to synchronously create a scope-owned instance.\n\n## Define A Component\n\n```ts\nimport { defineContract, procedure, createController } from '@emdash/wire/rpc';\nimport { defineWireComponent, requireContract } from '@emdash/wire/worker';\nimport { z } from 'zod';\n\nexport const clockContract = defineContract({\n  now: procedure({ input: z.void().optional(), output: z.number() }),\n});\n\nexport const counterContract = defineContract({\n  increment: procedure({ input: z.void().optional(), output: z.number() }),\n});\n\nconst component = defineWireComponent({\n  id: 'counter',\n  contract: counterContract,\n  requirements: {\n    clock: requireContract(clockContract),\n  },\n  configSchema: z.object({\n    initialValue: z.number().int().default(0),\n  }),\n  create: ({ scope, dependencies, config, instance }) => {\n    let value = config.initialValue;\n    scope.add(() => {\n      value = 0;\n    });\n\n    return instance({\n      scope,\n      controller: createController(counterContract, {\n        increment: async () => {\n          await dependencies.clock.now(undefined);\n          value += 1;\n          return value;\n        },\n      }),\n    });\n  },\n});\n```\n\n`create` is synchronous. Expected async work belongs in domain procedures, `LiveJobSource`s, machines,\nlive state, or other explicit runtime APIs. Directory creation, installs, probing, and startup\noperations should expose their own lifecycle and errors rather than hiding inside component\nconstruction.\n\n`instance({ scope, controller })` adapts the controller into a typed in-process client using a memory\ntransport. The controller should be unvalidated; the component creation boundary applies validation\naccording to the caller's `validate` policy.\n\n## Explicit Requirements\n\nRequirements are plain object keys. A component can require another Wire contract with\n`requireContract(contract)`. Deployment values such as paths, limits, executable names, and feature\nflags belong in typed component config, not dependency requirements.\n\nThere is no container, provider registry, singleton policy, recursive construction, or automatic\nresolution. Composition roots create or spawn the dependencies they want and pass dependency clients\nexplicitly. Worker hosts may also accept dependency controllers and forward them over dependency\nchannels.\n\n```ts\nconst clock = defineWireComponent({\n  id: 'clock',\n  contract: clockContract,\n  requirements: {},\n  configSchema: z.object({}),\n  create: ({ instance, scope }) =>\n    instance({\n      scope,\n      controller: createController(clockContract, {\n        now: () => Date.now(),\n      }),\n    }),\n});\n```\n\n## In-Process Creation\n\n`component.create({ scope, dependencies, config, validate })` validates config and dependency keys,\ncreates a child scope, wires a memory transport, serves the controller, and returns a typed client.\nDisposing the instance stops the in-memory server and disposes the component scope.\n\nController factories used by components should return unvalidated controllers. The component\ncreation or worker serving boundary applies validation once.\n\n```ts\nimport { createScope } from '@emdash/shared/concurrency';\n\nconst scope = createScope({ label: 'app' });\nconst clockInstance = clock.create({\n  scope,\n  dependencies: {},\n  config: {},\n  validate: 'inputs',\n});\n\nconst counterInstance = component.create({\n  scope,\n  dependencies: {\n    clock: clockInstance.client,\n  },\n  config: {\n    initialValue: 41,\n  },\n  validate: 'inputs',\n});\n\nawait counterInstance.client.increment(undefined); // 42\nawait scope.dispose();\n```\n\nThe caller owns the parent scope. Each component creates a child scope under it. Disposing either the\ninstance or the parent scope releases the component resources.\n\n## Worker Deployment\n\n`WireWorkerHost.create(component, options)` returns a lazy supervised worker. `worker.ready()`\nstarts it and returns the stable typed client. `spawn(component, options)` eagerly starts the\nworker and returns that client.\n\nWorker component IPC uses internal framed logical channels:\n\n- `runtime` for the served component contract.\n- bootstrap messages for config and readiness.\n- one `dep:<name>` channel per contract requirement.\n\nDependency channels are forwarded only from the explicit `dependencies` object supplied by the\ncomposition root. Child workers receive typed clients for those channels; they do not locate or\nstart dependencies themselves.\n\nParent process:\n\n```ts\nimport { createScope } from '@emdash/shared/concurrency';\nimport { createWireWorkerHost } from '@emdash/wire/worker';\nimport { childProcessSpawner } from '@emdash/wire/worker/node';\nimport { clock, component as counterComponent } from './component';\nimport { workerPath } from './worker-manifest';\n\nconst scope = createScope({ label: 'app' });\nconst host = createWireWorkerHost({\n  scope,\n  processSpawner: childProcessSpawner(),\n});\n\nconst clockInstance = clock.create({\n  scope,\n  dependencies: {},\n  config: {},\n});\n\nconst counterWorker = host.create(counterComponent, {\n  executable: workerPath('counter'),\n  dependencies: {\n    clock: clockInstance.client,\n  },\n  config: {\n    initialValue: 0,\n  },\n});\n\nconst counterClient = await counterWorker.ready();\nawait counterClient.increment(undefined);\n```\n\nWorker entry:\n\n```ts\nimport { runWireComponentWorker } from '@emdash/wire/worker';\nimport { component as counterComponent } from './component';\n\nvoid runWireComponentWorker(counterComponent);\n```\n\nThe client returned by `ready()` is stable across process generations. Calls fail while the worker is\nunavailable; the client does not buffer calls during restarts.\n\n## Unrecoverable Failures\n\nComponent code can call `fatal(error)` when process-local state is poisoned and the current instance\ncannot safely serve later requests. Fatal failures retire the component scope exactly once. An\nin-process caller can observe the failure through `onFatal`; `runWireComponentWorker` exits with code\n`1`, allowing the owning `WireWorkerHost` to replace that process generation under its supervision\npolicy.\n\nUse `fatal` only when replacing the instance is the recovery mechanism, such as a native operation\nthat remains pending after its deadline and cannot be cancelled in-process. Expected request errors\nshould still be returned or thrown through the contract. A fatal report does not make an unresponsive\nnative call safe to overlap with another call in the same process; worker deployment recovers by\nterminating the process that owns the native state.\n\n## Testing Components\n\nFor in-process tests, create the component under a test scope and pass fake dependency clients\nexplicitly.\n\n```ts\nimport { createScope } from '@emdash/shared/concurrency';\nimport { createController } from '@emdash/wire/rpc';\nimport { defineWireComponent } from '@emdash/wire/worker';\nimport { z } from 'zod';\n\nconst scope = createScope({ label: 'test' });\nconst fakeClockComponent = defineWireComponent({\n  id: 'fake-clock',\n  contract: clockContract,\n  requirements: {},\n  configSchema: z.object({}),\n  create: ({ instance, scope }) =>\n    instance({\n      scope,\n      controller: createController(clockContract, {\n        now: () => 1,\n      }),\n    }),\n});\nconst fakeClock = fakeClockComponent.create({\n  scope,\n  dependencies: {},\n  config: {},\n});\n\nconst counter = component.create({\n  scope,\n  dependencies: {\n    clock: fakeClock.client,\n  },\n  config: {\n    initialValue: 0,\n  },\n  validate: 'full',\n});\n\nawait expect(counter.client.increment(undefined)).resolves.toBe(1);\nawait scope.dispose();\n```\n\nFor worker tests, use `FakeWorkerProcessSpawner` and run the child side with\n`runWireComponentWorker(component, { port: fakeProcess.childPort })`. This exercises bootstrap,\nlogical channel isolation, dependency forwarding, readiness, and reconnect behavior without starting\na real child process.\n\n## Build Metadata\n\nManual worker manifests in the desktop app and workspace-server are temporary build metadata. They\nmap thin entry files to emitted worker artifacts until a bundler plugin owns worker artifact\ngeneration. They are not part of the component runtime API.\n\n## Guidelines\n\n- Keep component factories synchronous and cheap.\n- Report poisoned process-local state through `fatal`; keep expected operation failures on the\n  contract.\n- Pass dependencies by name from the composition root; never look them up from a global registry.\n- Prefer contract requirements for shared capabilities that may cross process boundaries.\n- Use typed config for deployment-specific values such as paths, limits, executable names, and\n  feature flags.\n- Keep controller validation at the component creation or worker serving boundary, not inside every\n  controller factory.\n- Keep manual worker manifests and entry files thin; they should only select a component and call\n  `runWireComponentWorker(component)`.\n","isInternal":false,"tokens":1905,"sizeBytes":9271},{"name":"lifecycle.md","path":"packages/wire/docs/runtime/lifecycle.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/packages/wire/docs/runtime/lifecycle.md","title":"Runtime Skill","category":"anthropic-skill","format":"markdown","content":"# Lifecycle Utilities\n\n`Scope`, `LifecycleRegistry`, and the resource cache primitives are\ndependency-light Shared lifecycle utilities exported from\n`@emdash/shared/concurrency`. Scheduling utilities are exported separately from\n`@emdash/shared/scheduling`. Wire uses these primitives, but they do not define\nWire protocol messages.\n\n- `Scope` owns cleanup and async work for a tree of resources.\n- `LifecycleRegistry` owns explicit keyed start/stop state machines.\n- `Clock` owns sleeps, deadlines, retry backoff, and disposable timers.\n- `ResourceCache` turns keyed demand into retained resources with ref-counted leases.\n- `SharedResource` is the unkeyed form for one lazily created resource.\n- `AsyncCache` caches async values that do not have finalizers.\n- `Mailbox` is a bounded async handoff queue for one logical consumer.\n\nThey do not define wire messages and can run in browser, renderer, main process,\nor tests.\n\n## Scope\n\nUse a scope when a feature creates more than one disposable resource and those\nresources should die together:\n\n```ts\nimport { createScope } from '@emdash/shared/concurrency';\n\nconst scope = createScope({ label: 'conversation:abc', logger });\n\nscope.add(() => detachLiveState());\nscope.add(() => removeWindowListener());\nscope.use({ dispose: () => runtime.dispose() });\nscope.run('refresh', async (signal) => {\n  await clock.sleep(250, { signal });\n  await refreshVisibleState({ signal });\n});\n\nawait scope.dispose();\n```\n\nSemantics:\n\n- `state` is `open`, `closing`, or `closed`.\n- `signal` aborts synchronously when disposal begins.\n- `run(label, fn)` starts a tracked async operation with its own abort signal.\n- Cleanups run in reverse registration order.\n- Child scopes and active runs settle before the parent's own cleanups.\n- Cleanup errors are reported through `onCleanupError` and do not stop later\n  cleanups.\n- `dispose()` is idempotent.\n- `add()` on an already disposed scope runs the cleanup immediately.\n- `Run.exit` never rejects; use `Run.value()` when ordinary promise rejection is\n  desired.\n\nThat last rule makes async attach/dispose races easier to handle:\n\n```ts\nconst scope = createScope();\nawait scope.dispose();\n\nscope.add(() => detachTopic()); // runs immediately\n```\n\n## Scope Logging\n\nEvery scope has `scope.log`. If a logger is supplied at the root, children\ninherit it with a `scope` binding for the accumulated label path:\n\n```ts\nconst runtimeScope = createScope({ label: 'runtime', logger });\nconst sessionScope = runtimeScope.child('session:one');\n\nsessionScope.log.info('session attached');\n```\n\nDefault cleanup error handling logs through the scope logger:\n\n```ts\nconst scope = createScope({ label: 'view', logger });\nscope.add(() => {\n  throw new Error('cleanup failed');\n});\n\nawait scope.dispose(); // logger.warn('scope cleanup failed', ...)\n```\n\nCustom cleanup handlers receive `{ label, labelPath, logger }`:\n\n```ts\nconst scope = createScope({\n  label: 'root',\n  onCleanupError: (error, context) => {\n    context.logger.warn('custom cleanup failure', {\n      labelPath: context.labelPath,\n      error,\n    });\n  },\n});\n```\n\nUse `describeScope(scope)` to inspect the live label tree:\n\n```ts\nconsole.log(describeScope(runtimeScope));\n```\n\nThe description contains labels, label paths, lifecycle state, active runs, and\nchild descriptions. It does not expose cleanup callbacks.\n\nWhen `createScope({ clock })` is used, child scopes inherit the same clock and\nrun diagnostics use deterministic timestamps.\n\n## Scope Runs\n\nUse `run()` when async work must not outlive the scope:\n\n```ts\nconst run = sessionScope.run('attach-transcript', async (signal) => {\n  const detach = await transcript.attach(updateView, { signal });\n  sessionScope.add(detach);\n});\n\nawait run.exit;\n```\n\nDisposing `sessionScope` cancels the run and waits for it before running the\nscope's cleanups. Cancellation is cooperative: pass the provided signal to any\noperation that can wait, sleep, perform I/O, or subscribe.\n\nUse `run()` for background jobs, process transitions, replica attachment,\nrenderer bindings, and async setup. Use `add()` or `use()` for finalizers. Use\n`ResourceCache` when a keyed resource should be shared by leases.\n\nFor the full lifecycle model and invariants, see\n[Structured concurrency](./structured-concurrency.md).\n\n## LifecycleRegistry\n\nUse `LifecycleRegistry` when a feature owns a keyed set of resources that are\nstarted and stopped by explicit commands and callers need observable lifecycle\nstate:\n\n```ts\nimport { createLifecycleRegistry } from '@emdash/shared/concurrency';\nimport { ok } from '@emdash/shared';\n\nconst runtimes = createLifecycleRegistry<\n  { workspaceId: string },\n  Runtime,\n  StartRuntimeError,\n  { reason: string },\n  StopRuntimeError\n>({\n  label: 'workspace-runtimes',\n  scope: appScope,\n  keyOf: (input) => input.workspaceId,\n  start: async (input, scope, signal) => {\n    const runtime = await createRuntime(input.workspaceId, { signal });\n    scope.add(() => runtime.dispose());\n    return ok(runtime);\n  },\n  stop: async (_key, runtime, context, _scope, signal) => {\n    await runtime.stop({ reason: context?.reason, signal });\n    return ok(undefined);\n  },\n  onStateChanged: ({ key, current }) => {\n    logger.info('runtime state changed', { key, state: current.kind });\n  },\n});\n```\n\nSelection guidance:\n\n- Use `Scope` for ownership, cancellation, cleanup ordering, and async work that\n  must not outlive its feature. It is not a keyed start/stop registry.\n- Use `LifecycleRegistry` for local keyed resources with explicit `start()`,\n  `stop()`, `register()`, `state()`, and state-change observation.\n- Use `ResourceCache` when demand is lease-driven and the resource should exist\n  only while consumers hold leases or while an idle TTL retains it.\n- Use `WireWorkerHost.create(component, ...)` when the resource is a Wire worker process with\n  readiness, a stable typed client returned by `ready()`, supervision, restart backoff, and child\n  process generations.\n- Use `LiveJobSource` when callers need a Wire-visible cancellable job with progress,\n  terminal state, retention, and remote client handles.\n\n`LifecycleRegistry` differs from `Scope` by modeling resource state rather than\ngeneral cleanup. It differs from `ResourceCache` by not ref-counting leases or\ncreating resources on demand from `acquire()`. It differs from Wire workers and\n`LiveJobSource` by staying local and protocol-free: it does not supervise processes,\nserve clients, publish progress, or retain terminal job state.\n\n## Mailbox Ownership\n\nUse `Mailbox` when local producers and one consumer need an explicit bounded\nhandoff. Register it with the owning scope, then run the consumer loop under the\nsame scope:\n\n```ts\nimport { createMailbox } from '@emdash/shared/concurrency';\n\nconst mailbox = sessionScope.use(createMailbox<Event>({ capacity: 256 }));\n\nsessionScope.run('drain-events', async () => {\n  for await (const event of mailbox) {\n    await handleEvent(event);\n  }\n});\n```\n\nDisposing `sessionScope` disposes the mailbox and unblocks pending producers or\nconsumers. For state machines, overflow guarantees, and the deferred Broadcast\ncontract, see [Mailbox and Broadcast](./mailbox-and-broadcast.md).\n\n## Child Scopes\n\nUse child scopes to model ownership:\n\n```ts\nconst runtimeScope = createScope({ label: 'runtime' });\nconst sessionScope = runtimeScope.child('session:one');\n\nsessionScope.add(() => stopSession());\nruntimeScope.add(() => stopRuntime());\n\nawait runtimeScope.dispose();\n```\n\nDisposing `runtimeScope` disposes `sessionScope` first, then `stopRuntime()`.\nDisposing `sessionScope` directly deregisters it from the parent so it does not\ndispose twice later.\n\n## ResourceCache\n\n`ResourceCache` is useful when a resource should exist only while somebody is\nusing it: a live topic binding, a renderer view model, a preview server, or a\nprocess-backed session.\n\n```ts\nimport { createResourceCache } from '@emdash/shared/concurrency';\n\nconst sessions = createResourceCache({\n  scope: runtimeScope,\n  label: 'sessions',\n  key: (input: { conversationId: string }) => input.conversationId,\n  idleTtlMs: 30_000,\n  create: async ({ conversationId }, scope) => {\n    const session = await startSession(conversationId);\n    scope.add(() => session.stop());\n    return session;\n  },\n  onError: (error, key) => logger.warn('session creation failed', { key, error }),\n});\n```\n\n`create(key, scope)` is also the disposal hook. Register teardown on the supplied\nscope as soon as resources are acquired. If creation later fails, the scope is\ndisposed and partial resources are cleaned up.\n\nBehavior:\n\n- The first `acquire()` for a key calls `create()`.\n- Concurrent acquires for the same key share the in-flight creation.\n- In-flight creation is tracked as a scope run and is cancelled when the entry is\n  invalidated or the parent scope closes.\n- Each lease increments the refcount.\n- `release()` is idempotent.\n- The last release starts the grace timer.\n- Acquire during the grace timer cancels teardown and reuses the active value.\n- Failed creation is not cached; the next acquire retries.\n- `peek(key)` returns the active value if creation has completed.\n- `dispose()` force-disposes every active or retained entry.\n- Passing `scope` attaches the source and all keyed entries to a parent scope.\n  Disposing that parent scope force-disposes the source and rejects later\n  acquisitions. `label` names the source node in `describeScope()` output.\n- Every input that affects provisioning must be in `K` or fixed in the factory\n  closure. There is no acquire-time creation context.\n\nUsage:\n\n```ts\nconst lease = sessions.acquire({ conversationId: 'abc' });\nconst session = await lease.ready();\n\nawait lease.release();\nawait sessions.dispose();\n```\n\nParent scopes are useful for runtimes with several keyed resources:\n\n```ts\nconst authScope = runtimeScope.child('auth');\nconst logins = createResourceCache({\n  scope: authScope,\n  label: 'login-source',\n  key: (key: { providerId: string; methodId: string; generation: string }) =>\n    `${key.providerId}:${key.methodId}:${key.generation}`,\n  create: async (key, scope) => {\n    const login = await startLoginPty(key.providerId, key.methodId);\n    scope.add(() => login.dispose());\n    return login;\n  },\n});\n\nawait authScope.dispose(); // also disposes every active login entry\n```\n\n## Grace Windows\n\nUse `idleTtlMs` for resources whose demand can flap briefly. For example, a window\nreload may detach and reattach live model bindings within a few hundred\nmilliseconds. A short grace window avoids tearing down the underlying resource\nonly to recreate it immediately.\n\n```ts\nconst bindings = createResourceCache({\n  key: (key: { taskId: string }) => key.taskId,\n  idleTtlMs: 5_000,\n  create: async (key, scope) => {\n    const binding = client.task.transcript(key, (state) => render(state));\n    scope.add(() => binding.dispose());\n    await binding.ready;\n    return binding;\n  },\n});\n```\n\nUse `ResourceCache`, `SharedResource`, or `AsyncCache` for new shared lifetime\nwork. Avoid adding compatibility lifecycle adapters around new code.\n\nSee [../../examples/scope/client.ts](../../examples/scope/client.ts) and\n[../../examples/resource-cache/client.ts](../../examples/resource-cache/client.ts).\n","isInternal":false,"tokens":2525,"sizeBytes":11220},{"name":"mailbox-and-broadcast.md","path":"packages/wire/docs/runtime/mailbox-and-broadcast.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/packages/wire/docs/runtime/mailbox-and-broadcast.md","title":"Runtime Skill","category":"anthropic-skill","format":"markdown","content":"# Mailbox and Broadcast\n\n`Mailbox<T>` is a Shared bounded asynchronous handoff primitive exported from\n`@emdash/shared/concurrency`. Wire uses it when one logical consumer needs to\ndrain values from one or more producers without hand-writing arrays, waiter\nlists, terminal flags, and overflow policy.\n\n```ts\nimport { createMailbox, createScope } from '@emdash/shared/concurrency';\n\nconst scope = createScope({ label: 'download' });\nconst chunks = scope.use(createMailbox<Uint8Array>({ capacity: 64 }));\n\nconst consumer = scope.run('drain', async () => {\n  for await (const chunk of chunks) {\n    await writeChunk(chunk);\n  }\n});\n\nawait chunks.offer(new Uint8Array([1, 2, 3]));\nchunks.close();\nawait consumer.value();\n```\n\n## Mailbox\n\nA mailbox has many producers and one logical consumer. Values accepted into the\nmailbox are delivered in FIFO order. The consumer can use `take()` for one value\nat a time or `for await` for a draining loop.\n\n```ts\nconst mailbox = createMailbox<Job>({\n  capacity: 128,\n  overflow: 'suspend',\n});\n\nawait mailbox.offer(job, { signal });\nconst next = await mailbox.take({ signal });\n```\n\n### State Machine\n\n```mermaid\nstateDiagram-v2\n  [*] --> Open\n  Open --> Open: offer / take\n  Open --> Closing: close()\n  Closing --> Closed: buffer drained\n  Open --> Failed: fail(error)\n  Failed --> Failed: drain accepted values\n  Failed --> [*]: next take throws error\n  Closed --> [*]\n```\n\n`close()` is graceful: accepted values drain before consumers see the end of the\nstream. `fail(error)` is also ordered: accepted values drain first, then the next\ntake or iterator step throws the failure. `dispose()` is immediate: it clears\nbuffered values and unblocks pending producers and consumers.\n\n### Guarantees\n\n- Accepted values are delivered in FIFO order.\n- A value is delivered to at most one consumer.\n- Only one pending `take()` or active async iterator is allowed at a time.\n- Capacity is explicit and always finite.\n- Overflow is explicit: `suspend`, `reject`, `drop-oldest`, or `drop-newest`.\n- `tryOffer()` never waits.\n- `offer()` waits only when overflow is `suspend`.\n- `offer()` and `take()` can observe an `AbortSignal`; aborting one operation does\n  not close the mailbox.\n- Terminal operations are idempotent.\n- `scope.use(mailbox)` ties disposal to the owning scope, but consumer loops should\n  still run under `scope.run()` when they must not outlive the feature.\n\n### Overflow Policies\n\nUse `suspend` when every accepted value matters and producers can wait. Use\n`reject` when overflow is a protocol or invariant violation. Use `drop-oldest` for\nbest-effort queues where freshness matters more than history. Use `drop-newest`\nwhen existing queued work should be preserved and incoming work can be ignored.\n\nBlob channels use `reject` internally because dropping bytes would corrupt the\nstream. The reconnecting transport uses a shared bounded buffer with\n`drop-oldest`, preserving the existing policy that old non-blob frames are less\nvaluable than recent ones while disconnected.\n\n### Non-Goals\n\nMailbox is not:\n\n- a durable queue;\n- a multi-consumer work queue;\n- a replay log;\n- an acknowledgement or retry protocol;\n- a mutex;\n- a domain error channel.\n\nExpected domain failures should still be represented as `Result<T, E>`. Mailbox\nfailure means the local handoff itself was terminated by lifecycle, protocol, or\ntransport failure.\n\n## Broadcast Contract\n\n`Broadcast<T>` is the reserved name for a future local asynchronous fan-out\nprimitive. It should not be called `Topic<T>` because Wire already uses “topic” for\nremote live subscription addresses.\n\nThe intended shape is one mailbox per subscriber:\n\n```mermaid\nflowchart LR\n  publisher[Publisher] --> broadcast[Broadcast]\n  broadcast --> subA[\"Subscriber A Mailbox\"]\n  broadcast --> subB[\"Subscriber B Mailbox\"]\n  broadcast --> subC[\"Subscriber C Mailbox\"]\n```\n\nThe intended guarantees are:\n\n- `publish(value)` never runs subscriber application code directly.\n- Each subscriber receives values through its own mailbox.\n- Slow subscribers apply only their own overflow policy.\n- Per-subscriber delivery is FIFO for accepted values.\n- New subscribers do not receive replay.\n- Closing or failing the broadcast propagates to every subscription.\n- Publish results report subscriber count, delivered count, and dropped count.\n\nDo not implement or migrate to `Broadcast<T>` until a source has at least two\ngenuinely asynchronous consumers that need independent buffering or overflow\npolicies. Current Wire sources do not meet that bar: process stdio has one\nproduction subscriber, and observability hooks already isolate synchronous\nfailures.\n\n## Where Not To Use These\n\nDo not replace `LiveStateSource`, `LiveLogSource`, `EventStreamSource`, replicas, live\nfollowers, or reconnect attachment logic with Mailbox or Broadcast. Those\nprimitives carry generation, sequence, snapshot, resync, reconnect, retention, or\ngap semantics that Mailbox deliberately does not provide.\n\nDo not replace condition waiters such as live cursor waiters or renderer version\nwaiters. They wait for a predicate over monotonic state; they do not queue values.\n\nSee also [Structured concurrency](./structured-concurrency.md) and\n[Lifecycle utilities](./lifecycle.md).\n","isInternal":false,"tokens":1158,"sizeBytes":5240},{"name":"resource-cache.md","path":"packages/wire/docs/runtime/resource-cache.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/packages/wire/docs/runtime/resource-cache.md","title":"Runtime Skill","category":"anthropic-skill","format":"markdown","content":"# Resource Caches\n\n`ResourceCache`, `SharedResource`, and `AsyncCache` are Shared primitives from\n`@emdash/shared/concurrency`. Wire uses them for shared lifetimes and cached\nasync values, but they do not define Wire protocol messages.\n\n## Choose A Primitive\n\n- Use `ResourceCache<K, T>` for keyed resources that need leases and teardown:\n  process sessions, replicas, file watchers, tree resources, and connection pools.\n- Use `SharedResource<T>` for one lazily created resource with the same lease\n  semantics: a lazy worker, singleton client, or shared runtime helper.\n- Use `AsyncCache<K, T>` for data without finalizers: auth status probes,\n  metadata lookups, or other retryable reads.\n- Use `deduplicate()` middleware only when you want in-flight sharing without\n  retaining successful values.\n\n## Resource Identity\n\nEvery input that can change provisioning must be part of `K` or captured by the\nfactory closure. The cache only receives `acquire(key)`, so there is no separate\ncreation context:\n\n```ts\nconst logins = createResourceCache({\n  key: (key: { providerId: string; methodId: string; generation: string }) =>\n    `${key.providerId}:${key.methodId}:${key.generation}`,\n  create: async (key, scope) => {\n    const pty = await startLoginPty(key.providerId, key.methodId);\n    scope.add(() => pty.dispose());\n    return pty;\n  },\n});\n```\n\n## Scope Ownership\n\nEach cache owns an internal scope. Each entry gets a child scope, and creation runs\nunder `scope.run('create', ...)`. Register teardown on the entry scope as soon as\nresources are acquired. If creation fails, the entry scope is disposed and the\nnext acquire retries.\n\n`invalidate(key)` and `dispose()` force-close entries even if leases are still\nheld. Releasing a stale lease later is a no-op. This keeps owner shutdown from\nwaiting forever on leaked consumers.\n\n## Lease Behavior\n\n- Concurrent `acquire(key)` calls share one in-flight creation.\n- Every acquire receives its own idempotent lease.\n- `peek(key)` returns only completed active values.\n- Failed creation is not cached.\n- By default, releasing the final lease during creation retains the in-flight entry so a later\n  acquire can share it and the factory's terminal result remains authoritative.\n- Set `cancelPendingOnRelease: true` for resources such as watchers that have no useful lifetime\n  without a lease. Pending work must then observe the entry scope's signal and clean up late results.\n- `idleTtlMs` retains a zero-ref entry briefly so flapping demand can reuse it.\n- Acquiring during teardown waits for teardown to finish, then provisions fresh.\n\n## AsyncCache\n\n`AsyncCache` shares concurrent loads and caches only successes:\n\n```ts\nconst statuses = createAsyncCache({\n  key: (providerId: string) => providerId,\n  ttlMs: 15 * 60 * 1000,\n  load: (providerId, signal) => checkAuthStatus(providerId, { signal }),\n});\n```\n\n`get()` uses a cached value when fresh, `refresh()` cancels the current load and\nstarts a new generation, `set()` publishes an explicit value, and failures are\nnever retained.\n","isInternal":false,"tokens":691,"sizeBytes":3038},{"name":"scheduling.md","path":"packages/wire/docs/runtime/scheduling.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/packages/wire/docs/runtime/scheduling.md","title":"Runtime Skill","category":"anthropic-skill","format":"markdown","content":"# Scheduling\n\n`@emdash/shared/scheduling` centralizes time, retry, and\ncancellation-sensitive waits. Wire uses these Shared primitives, and runtime\ncode should depend on `Clock` instead of calling platform timer APIs directly.\n\n```ts\nimport { retry, retrySchedules, systemClock } from '@emdash/shared/scheduling';\n\nawait retry(\n  () => transport.connect(),\n  {\n    clock: systemClock,\n    schedule: retrySchedules.exponential({ initialMs: 100, maxMs: 2_000 }),\n    shouldRetry: (error) => isTransient(error),\n  }\n);\n```\n\n## Clock\n\n`Clock` has three operations:\n\n- `now()` returns wall-clock milliseconds for diagnostics and deadlines.\n- `schedule(delayMs, callback)` returns a disposable `TimerHandle`.\n- `sleep(delayMs, { signal })` returns an abortable promise.\n\nUse `Clock` for sleeps, backoff, debounce, retention, idle TTLs, readiness\ndeadlines, and test polling. Keep direct `Date.now()` or `performance.now()` for\nprotocol timestamps, generated IDs, and duration measurement where the wall clock\nor monotonic timer is the data being recorded.\n\n## TimerHandle Ownership\n\nEvery scheduled callback must have one owner. Register the handle with a `Scope`,\nstore it on the owning entry, or dispose it in all terminal paths:\n\n```ts\nconst handle = clock.schedule(5_000, () => expire(id), { unref: true });\nscope.add(() => handle.dispose());\n```\n\n`TimerHandle.dispose()` is idempotent. The `active` flag becomes false before the\ncallback runs, so callback and disposal races remain at-most-once.\n\n## Timeouts\n\n`runWithTimeout(work, { timeoutMs, signal, clock })` derives a child\n`AbortSignal`, schedules a deadline on the provided `Clock`, and disposes its\ntimer in every terminal path. The returned promise rejects with `TimeoutError`\nwhen the deadline expires, but parent cancellation still rejects with the\nparent's abort reason.\n\n```ts\nconst result = await runWithTimeout(\n  (signal) => loadWorkspace({ signal }),\n  { timeoutMs: 5_000, signal: parentSignal, clock }\n);\n```\n\nThe timeout rejects promptly even when work ignores the derived signal, but the\nunderlying JavaScript work can only stop cooperatively. Pass the derived signal\ninto sleeps, process waits, transports, and any helper that can observe\ncancellation.\n\n## ManualClock\n\nTests should import `ManualClock` from `@emdash/shared/testing`. It preserves FIFO\nordering for timers with the same deadline and flushes resumed promise\nmicrotasks while advancing:\n\n```ts\nconst clock = new ManualClock(1_000);\nconst done = clock.sleep(250);\n\nawait clock.advanceBy(250);\nawait done;\n```\n\nUse `runAll({ maxTimers })` only when the test expects the scheduled work to\ndrain. The guard prevents accidental infinite timer loops.\n\n## Retry Semantics\n\n`RetrySchedule.delayFor(0)` is the delay before the first retry. Attempt `0` is\nthe initial operation call, attempt `1` is the first retry, and so on.\n`undefined` means retries are exhausted.\n\nSchedules are stateless and composable: fixed, finite sequence, repeat-last\nsequence, exponential, limited, and deterministic jitter. Inject a deterministic\nrandom function into jittered schedules in tests.\n\n`retry()` is exception-based. Domain errors that are expected API results should\nremain `Result<T, E>` values; use `shouldRetry` to classify thrown transport or\nruntime failures.\n\n## Wire Retry Vocabulary\n\nWire expresses all retry timing through one schedule vocabulary:\n`RetrySchedule` and the `retrySchedule()` constructor from\n`@emdash/shared/scheduling`. The reconnecting transport's reconnect delays,\nworker-slot supervision, the follower's `resyncRetry` policy, and per-call\nopt-in retries all consume it. Shared vocabulary, not shared state — each site\nkeeps its own independently tunable schedule:\n\n```ts\nimport { retrySchedule } from '@emdash/shared/scheduling';\n\nconst supervision = {\n  restart: 'on-failure',\n  schedule: retrySchedule({ delaysMs: [250, 1_000, 2_500], repeatLast: true, maxRetries: 5 }),\n};\n```\n\n## Periodic Work\n\nPrefer abortable loops or chained one-shot timers over overlapping intervals.\nThat keeps disposal ordered and avoids installing work after an owner has begun\nclosing:\n\n```ts\nscope.run('poll', async (signal) => {\n  while (!signal.aborted) {\n    await refresh({ signal });\n    await clock.sleep(1_000, { signal });\n  }\n});\n```\n","isInternal":false,"tokens":983,"sizeBytes":4270},{"name":"structured-concurrency.md","path":"packages/wire/docs/runtime/structured-concurrency.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/packages/wire/docs/runtime/structured-concurrency.md","title":"Runtime Skill","category":"anthropic-skill","format":"markdown","content":"# Structured Concurrency\n\nShared `Scope.run()` from `@emdash/shared/concurrency` tracks asynchronous work\nunder the same owner as subscriptions, processes, timers, and other resources.\nWire uses it when work must not outlive the feature that started it.\n\n```ts\nconst scope = createScope({ label: 'session:abc', logger, clock });\n\nconst run = scope.run('load-history', async (signal) => {\n  const history = await loadHistory({ signal });\n  return history;\n});\n\nconst history = await run.value();\nawait scope.dispose();\n```\n\n## Concepts\n\n- `Scope` owns a lifecycle tree. Parent disposal closes children.\n- `Run<A>` is one async operation owned by a scope.\n- `Run.exit` never rejects. It resolves to `success`, `failure`, or `cancelled`.\n- `Run.value()` converts the exit back to normal promise semantics.\n- `Lease<A>` is temporary use of a shared resource, usually from `ResourceCache`\n  or `SharedResource`.\n\n```mermaid\nflowchart TD\n  featureScope[\"Feature Scope\"] --> childScope[\"Child Scope\"]\n  featureScope --> runA[\"Run: load-history\"]\n  childScope --> runB[\"Run: attach-topic\"]\n  childScope --> cleanup[\"Finalizers\"]\n```\n\n## Lifecycle\n\nDisposal is idempotent and ordered:\n\n1. The scope moves from `open` to `closing`.\n2. `scope.signal` aborts synchronously.\n3. Existing child scopes begin closing.\n4. Active runs are cancelled.\n5. The scope waits for child scopes and runs.\n6. Cleanups run in reverse registration order.\n7. Cleanup errors are logged and later cleanups still run.\n8. The scope moves to `closed`.\n\nStarting a run after closing has begun does not invoke the operation. The returned\nrun is already cancelled. Adding a cleanup after a scope is closed runs it\nimmediately as best-effort cleanup.\n\n## Invariants\n\n- Every child scope has one parent.\n- Every run belongs to one scope.\n- `dispose()` returns the same promise for every caller.\n- `scope.signal` aborts before the first `await` in disposal.\n- Parent cancellation propagates to children.\n- Sibling runs do not cancel each other.\n- `Run.exit` never rejects.\n- A cancelled run is reported as `cancelled` even if the operation later returns.\n- A failed run is logged through the scope logger.\n- A scope is not closed until owned runs and children have settled.\n\nJavaScript cancellation is cooperative. A run that ignores its signal can keep\n`dispose()` pending. Pass the run signal into network calls, sleeps, process\nwaits, and any helper that can observe cancellation.\n\n`Scope` accepts an optional `Clock`. Child scopes inherit it, and active-run\ndiagnostics use `clock.now()` for deterministic timestamps in tests. Prefer\n`clock.sleep(ms, { signal })` for run-owned delays so disposal can interrupt the\nwait immediately.\n\nA run must not await disposal of its own owning scope. That creates a cycle:\n`dispose()` waits for the run, while the run waits for `dispose()`.\n\n## When To Use It\n\nUse `run()` for:\n\n- background jobs and progress sources;\n- pending replica attachment or snapshot work;\n- process spawn, restart backoff, and exit handling;\n- renderer bindings with fire-and-forget RPCs;\n- async setup that must be cleaned up if later setup fails.\n\nUse `add()` or `use()` for:\n\n- subscriptions;\n- timers and observers;\n- resource finalizers;\n- objects with `dispose()`.\n\nUse child scopes for:\n\n- per-session resources under a runtime;\n- per-job resources under a job source;\n- per-consumer resources under a shared cache;\n- modal or view-local lifetimes.\n\nUse `ResourceCache` for keyed, ref-counted resources. Its creation runs under\nthe entry scope, so invalidating an entry or disposing the parent scope cancels\nand joins in-flight creation before finalizers run.\n\nUse `Mailbox` for bounded asynchronous handoff between producers and one logical\nconsumer. The mailbox owns buffering and terminal-state wakeups; run the consumer\nloop under `scope.run()` when it must not outlive the feature:\n\n```ts\nconst mailbox = scope.use(createMailbox<Message>({ capacity: 128 }));\n\nscope.run('drain messages', async () => {\n  for await (const message of mailbox) {\n    await handleMessage(message);\n  }\n});\n```\n\n`Broadcast<T>` is documented as a future local fan-out primitive for multiple\nasynchronous consumers with independent backpressure. Do not use Mailbox or\nBroadcast to replace live-state snapshots, retained logs, event streams, replica\ncursors, or reconnect attachment logic; those protocols carry generation,\nsequence, retention, or resync semantics.\n\nDo not use `run()` as:\n\n- a mutex or serialization primitive;\n- a retry policy;\n- a cache;\n- a domain error channel.\n\nExpected domain failures should still be returned as `Result<T, E>`. A run\nfailure means the operation itself threw unexpectedly.\n\n## Diagnostics\n\n`describeScope(scope)` includes the lifecycle state, active child scopes, and\nactive runs:\n\n```ts\nconsole.log(describeScope(scope));\n```\n\nEach run description includes its label, start time, and whether cancellation has\nstarted. This is useful for finding retained sessions, stuck process transitions,\nor long-running renderer bindings during tests and debugging.\n","isInternal":false,"tokens":1119,"sizeBytes":5060},{"name":"workers.md","path":"packages/wire/docs/runtime/workers.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/packages/wire/docs/runtime/workers.md","title":"Runtime Skill","category":"anthropic-skill","format":"markdown","content":"# Workers\n\nA worker is a named, process-hosted `WireComponent` owned by a `WireWorkerHost`.\nThe host creates logical workers and returns a stable typed client from readiness. Internally, each worker has one\nslot that owns readiness, supervision, reconnects, graceful shutdown, and process generations.\nLow-level spawners create exactly one process generation.\n\nSee [Components](./components.md) for the authored component shape, local creation examples,\ndependency requirements, and testing guidance. This page focuses on process supervision.\n\n## Layers\n\n```mermaid\nflowchart LR\n  App[\"Application composition\"] --> Host[\"WireWorkerHost\"]\n  Host --> Slot[\"Internal worker slot\"]\n  Slot --> Link[\"Internal worker link\"]\n  Slot --> Spawner[\"WorkerProcessSpawner\"]\n  Spawner --> Process[\"WorkerProcess\"]\n  Process --> Runner[\"runWireComponentWorker\"]\n  Runner --> Controller[\"Component Controller\"]\n  Host --> Deps[\"Explicit dependency channels\"]\n```\n\n- `WireWorkerHost` is a process ownership boundary. Composition roots inject a\n  `WorkerProcessSpawner`, logger, clock, and defaults.\n- The internal worker slot is the only multi-generation state machine. It keeps one stable\n  connection/client while child processes restart underneath it; it is not an author-facing API.\n- `WorkerProcessSpawner` is platform-specific and creates one generation. Use\n  `childProcessSpawner()` from `@emdash/wire/worker/node` for Node child\n  processes.\n- `runWireComponentWorker(component)` is the child-side IPC bridge. It bootstraps typed config,\n  connects explicit dependency clients, creates the component, serves its controller, and signals\n  readiness.\n\n## Parent Side\n\n```ts\nimport { createWireWorkerHost } from '@emdash/wire/worker';\nimport { childProcessSpawner } from '@emdash/wire/worker/node';\nimport { createScope } from '@emdash/shared/concurrency';\nimport { counterComponent } from './component';\nimport { workerPath } from './worker-manifest';\n\nconst scope = createScope({ label: 'main' });\nconst host = createWireWorkerHost({\n  scope,\n  processSpawner: childProcessSpawner(),\n});\n\nconst worker = host.create(counterComponent, {\n  name: 'counter',\n  executable: workerPath('counter'),\n  dependencies: {},\n  config: {},\n});\n\nconst client = await worker.ready();\nawait client.increment(undefined);\nawait host.dispose();\n```\n\n`worker.ready()` starts the process, waits for the child ready signal, and returns the stable\ntyped client. The pending worker intentionally exposes no public client. Calls through a previously\nreturned client fail fast while the worker is unavailable; Wire does not buffer requests during\ndowntime.\n\nStartup and exposure are explicit composition-site decisions. For eager startup, await\n`worker.ready()` under an owning scope or call `host.spawn(component, options)` to get the client. For Electron\nwindows, compose a forwarding controller and use `exposeWireToWindows()` with a `beforeOpen` hook\nthat awaits `worker.ready()`.\n\n## Child Side\n\n```ts\nimport { runWireComponentWorker } from '@emdash/wire/worker';\nimport { counterComponent } from './component';\n\nvoid runWireComponentWorker(counterComponent);\n```\n\nThe child helper resolves the parent IPC channel, requests bootstrap config and dependency channels,\nserves framed runtime wire messages, sends the ready signal after the component controller is\ninstalled, disposes the child scope on shutdown/disconnect, and exits with code `1` if creation\nfails. It also exits with code `1` when the component reports an unrecoverable failure through\n`fatal(error)`. The worker slot treats that exit like any other failed generation: it disconnects the\nold transport and applies the configured supervision schedule before starting a fresh process.\n","isInternal":false,"tokens":788,"sizeBytes":3718},{"name":"path-system.md","path":"agents/architecture/path-system.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/agents/architecture/path-system.md","title":"Subagent: path-system","category":"subagent-persona","format":"markdown","content":"# Path System\n\n`@emdash/core/primitives/path/api` is the source of truth for portable file identity and lexical\npath operations. The detailed package docs live in\n[`packages/core/docs/path/README.md`](../../packages/core/docs/path/README.md).\n\n## Ownership\n\n- `packages/core/src/path/` owns pure structured path primitives, resource URI\n  encoding, and comparison keys.\n- Desktop placement resolution owns repository and worktree location policy. It combines the target\n  host's home directory from `files.getHomeDir`, desktop settings, per-project overrides, and UI\n  input, then sends absolute paths to the host runtime.\n- Repository destinations default to `~/emdash/repositories/<name>` with numeric suffix allocation.\n  Worktree pools default to `~/emdash/worktrees/<repo-basename>-<hash8(repo-path)>`; the workspace\n  planner adds the sanitized branch leaf.\n- Runtime and app layers own host resolution, native string conversion,\n  filesystem I/O, realpath checks, authorization, and persistence migrations.\n- Renderer code should eventually prefer `ResourceUri` or structured refs over\n  raw absolute strings, but existing consumers are not migrated by the foundation\n  module.\n\n## Representation Rules\n\n- Use `HostFileRef` for a full file address on a host.\n- Use `ScopedPath` for root-relative operations such as Git paths, watcher\n  events, tree entries, and bulk calls.\n- Use `ResourceUri` for serialized identity.\n- Use `ResourceKey` for in-memory dedupe and maps.\n- Use explicit `PathProfile` semantics instead of `node:path` behavior when\n  interpreting cross-host paths.\n\n## Future Adoption Map\n\n- Core `files` path helpers can delegate lexical operations to\n  `@emdash/core/primitives/path/api`.\n- Desktop `RuntimePath` and SSH path helpers can become adapters around\n  structured path parsing/formatting.\n- Git models can represent repo-relative file coordinates as\n  `PortableRelativePath`.\n- fs-watch can normalize native events into `ScopedPath` before batching and\n  coalescing.\n- workspace-server schemas can move from documented string conventions to\n  structured path schemas.\n- Renderer tabs, tree nodes, Monaco models, comments, and view-state snapshots\n  can migrate from raw path strings to `ResourceUri`.\n\nDo not add filesystem I/O, `realpath()`, host registry state, or app-specific\nauthorization to `packages/core/src/path/`.\n","isInternal":false,"tokens":504,"sizeBytes":2357},{"name":"README.md","path":"agents/README.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/agents/README.md","title":"Subagent: readme","category":"subagent-persona","format":"markdown","content":"# Agent Docs\n\nThis directory is the system of record for agent-facing repo guidance. Keep topic pages small, specific, and mechanically checkable where possible.\n\n## Recommended Reading Order\n\n1. `quickstart.md`\n2. `architecture/overview.md`\n3. the task-specific page for the area you are changing\n\n## Directory Layout\n\n- `architecture/`\n  - system structure and major code ownership boundaries\n  - [`core-modules.md`](architecture/core-modules.md) — Core runtime, service, and primitive module types, platform surfaces, dependency rules, and subpath exports\n  - [`workspace-server.md`](architecture/workspace-server.md) — protocol versioning, negotiation handshake, and behavior-change rules for the remote workspace daemon\n  - [`state-ownership.md`](architecture/state-ownership.md) — desktop-owned entities, host-owned resources, bindings, and the placement rule for operation behavior\n  - [`acp-runtime.md`](architecture/acp-runtime.md) — ACP runtime, session manager, connection pool, cells, live models, and API contract ownership\n  - [`git-runtime.md`](architecture/git-runtime.md) — Git contract/runtime ownership, session lifecycle, and nested wire composition\n  - [`path-system.md`](architecture/path-system.md) — host-aware path identity, resource URI ownership, and future migration boundaries\n- `workflows/`\n  - task-oriented procedures like testing, worktrees, remote development, and Nx task orchestration\n- `integrations/`\n  - provider, MCP, and external service guidance\n  - [`integration-plugins.md`](integrations/integration-plugins.md) — integration auth and credential contracts, stable account identity, issue-, pull-request-, and repository-plugin contracts, remote-to-integration routing\n- `risky-areas/`\n  - places where incorrect changes are expensive\n- `conventions/`\n  - coding contracts and repo rules\n  - [`ui-kit.md`](conventions/ui-kit.md) — which UI kit to use: `@emdash/ui` for components, feature Tailwind for layout one-offs, where new primitives go, `cn()` location, theming\n  - [`ui-styling.md`](conventions/ui-styling.md) — vanilla-extract styling rules for `packages/ui`\n\n## Maintenance Rules\n\n- Prefer one page per concrete topic.\n- Avoid volatile counts unless you can verify them cheaply.\n- Link to the source-of-truth file paths.\n- Update the smallest relevant page instead of expanding `AGENTS.md`.\n","isInternal":false,"tokens":508,"sizeBytes":2362},{"name":"README.md","path":"apps/emdash-desktop/src/core/primitives/commands/README.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/apps/emdash-desktop/src/core/primitives/commands/README.md","title":"Command: /readme","category":"command-prompt","format":"markdown","content":"# Commands\n\nCommands are portable, schema-backed action definitions. Definitions contain\nstatic metadata and optional keybindings; view scopes provide their runtime\navailability and implementation.\n\n```ts\nconst createBranchCommand = defineCommand({\n  id: 'task.createBranch',\n  title: 'Create Branch',\n  category: 'Git',\n  input: z.object({ branchName: z.string(), baseRef: z.string() }),\n});\n```\n\nCommand input describes arguments supplied for one invocation. Scope params\ndescribe the context in which the command runs; callers must not duplicate\nscope context in command input.\n","isInternal":false,"tokens":118,"sizeBytes":581},{"name":"README.md","path":"packages/core/docs/path/README.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/packages/core/docs/path/README.md","title":"path Documentation","category":"plugin-manifest","format":"markdown","content":"# Path System\n\n`@emdash/core/primitives/path/api` defines Emdash's portable file identity vocabulary. It is\npure TypeScript, browser-safe, and does not perform filesystem I/O.\n\nUse this module when a path crosses a process, renderer, persistence, or Wire\nboundary. Runtime adapters remain responsible for converting structured paths\ninto native strings immediately before filesystem, Git, watcher, or process\nspawn operations.\n\n## Concepts\n\n- `HostRef` from `@emdash/core/primitives/host/api` selects a local or remote runtime host.\n- `HostAbsolutePath` is a structured absolute path with an explicit root:\n  POSIX root, Windows drive, or UNC share.\n- `PortableRelativePath` is a normalized `/`-separated coordinate relative to a\n  known root. The empty string represents the root.\n- `HostFileRef` combines `HostRef` and `HostAbsolutePath` into a routeable file\n  address.\n- `ScopedPath` combines a root `HostFileRef` and `PortableRelativePath`.\n- `ResourceUri` is the versioned string form for serialization and persistence.\n- `ResourceKey` is an opaque comparison key for maps and deduplication.\n\n## Basic Flow\n\n```ts\nimport {\n  encodeResourceUri,\n  hostFileRef,\n  parseAbsolute,\n  parsePortableRelativePath,\n  resolveScopedPath,\n  scopedPath,\n} from '@emdash/core/primitives/path/api';\nimport { LOCAL_HOST_REF } from '@emdash/core/primitives/host/api';\n\nconst rootPath = parseAbsolute('/repo', { profile: { style: 'posix' } });\nconst relative = parsePortableRelativePath('src/index.ts');\n\nif (rootPath.success && relative.success) {\n  const root = hostFileRef(LOCAL_HOST_REF, rootPath.data);\n  const scoped = scopedPath(root, relative.data);\n  const resolved = resolveScopedPath(scoped);\n\n  if (resolved.success) {\n    const uri = encodeResourceUri(resolved.data);\n    // emdash-file://v2/local/local/posix/repo/src/index.ts\n  }\n}\n```\n\n`HostFileRef` belongs at routing, persistence, and renderer identity boundaries.\nOnce a domain controller has selected a host-bound Wire connection, Git and Files\nruntime contracts carry `HostAbsolutePath` roots and `PortableRelativePath`\ncoordinates without repeating `HostRef` in every request.\n\n## Related Pages\n\n- [Normalization](normalization.md)\n- [Resources](resources.md)\n- [Boundaries](boundaries.md)\n- [Recipes](recipes.md)\n","isInternal":false,"tokens":523,"sizeBytes":2273},{"name":"README.md","path":"packages/core/docs/README.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/packages/core/docs/README.md","title":"docs Documentation","category":"plugin-manifest","format":"markdown","content":"# @emdash/core Docs\n\n`@emdash/core` contains transport-agnostic primitives shared by the desktop app,\nworkspace server, worker processes, and provider runtimes.\n\n## Public Exports\n\n- [`@emdash/core/primitives/path/api`](path/README.md) - pure host-aware path identity,\n  normalization, resource URI, and comparison helpers.\n\n## Boundaries\n\nCore package docs describe reusable primitives. App-specific wiring, persistence\nmigrations, Electron IPC, and host runtime ownership stay in the app or workspace\nserver packages that own those concerns.\n","isInternal":false,"tokens":114,"sizeBytes":544},{"name":"README.md","path":"packages/core/README.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/packages/core/README.md","title":"core Documentation","category":"plugin-manifest","format":"markdown","content":"# @emdash/core\n\n`@emdash/core` owns Emdash's shared domain APIs and the host-scoped runtime and\nservice implementations used by the desktop app and workspace-server. It is the\npackage for code that must be shared across hosts without putting that ownership\nin an app package.\n\nCore keeps domain definitions close to their implementations, but separates\nportable APIs from platform-specific code through explicit subpath exports. A\nconsumer should be able to tell from the import path whether it is importing\nshared vocabulary, a Node implementation, or a browser implementation.\n\n## Package Structure\n\nSource modules live under `src/` and are grouped by module type:\n\n```text\nsrc/\n  primitives/\n  services/\n  runtimes/\n  workspace-server/\n```\n\n### Primitives\n\n`primitives/` contains reusable vocabulary and small infrastructure that does\nnot own a product domain. Primitives may define schemas, value objects,\ndeterministic helpers, and narrowly scoped policies.\n\nExamples include host-aware paths, host identity, skill schemas, MCP schemas,\nagent environment helpers, plugin file-system ports, and concurrency helpers.\n\nPrimitives may depend on other primitives, but they must not depend on services\nor runtimes.\n\n### Services\n\n`services/` contains focused capabilities that can be injected into runtimes or\nhost composition code. Services may own bounded resources, expose ports, provide\nNode or browser implementations, or define Wire contracts for smaller shared\ncapabilities.\n\nExamples include execution contexts, PTY management, filesystem watching, host\ndependency detection, and agent plugin support.\n\nServices may depend on primitives and explicitly lower-level services. They must\nnot depend on runtimes.\n\n### Runtimes\n\n`runtimes/` contains host-scoped composition roots for larger product domains.\nA runtime typically implements a public Wire contract, owns domain state and\nresources, and coordinates injected services and primitives.\n\nCurrent runtime domains include:\n\n- `acp`\n- `agent-config`\n- `files`\n- `git`\n- `tui-agents`\n- `workspace`\n\nRuntimes are peers. A runtime must not import another runtime; cross-runtime\ncomposition belongs in the desktop host, workspace-server host, or another\nexplicit composition root.\n\n### Workspace Server\n\n`workspace-server/` is a special composition root, not a fourth general module\ntype. It owns workspace-server protocol versions, shared host schemas, aggregate\nWire contracts, and host-owned composition helpers. It may compose runtime APIs,\nservices, and primitives.\n\n## Surface Conventions\n\nEach module is split by platform surface:\n\n```text\n<module-type>/<module-name>/\n  api/\n  node/\n  browser/\n```\n\nModules only add the surfaces they need.\n\n### `api`\n\n`api/` is the portable public surface. It may contain Wire contracts, serialized\nmodels, DTOs, Zod schemas, public ports, typed errors, and pure helpers needed by\nconsumers.\n\n`api/` must not import `node/` or `browser/`, use platform modules, spawn\nprocesses, or own background work.\n\n### `node`\n\n`node/` contains Node-specific implementations: runtime and service factories,\nWire components, Wire controllers, procedure implementations, filesystem/Git/PTY/subprocess\nbehavior, and resource allocation. App-owned worker entry files adapt components to process hosting\nwith `runWireComponentWorker()`.\n\n### `browser`\n\n`browser/` contains browser-specific implementations of portable APIs. Browser\nsurfaces may import `api/` and other `browser/` surfaces, but must not import\nNode code.\n\n## Subpath Exports\n\nPublic imports must select an explicit module type, module name, and surface:\n\n```text\n@emdash/core/<module-type>/<module-name>/<surface>\n```\n\nExamples:\n\n```ts\nimport { gitContract } from '@emdash/core/runtimes/git/api';\nimport { GitRuntime, gitComponent } from '@emdash/core/runtimes/git/node';\n\nimport type { IWatchService } from '@emdash/core/services/fs-watch/api';\nimport { createNativeWatchService } from '@emdash/core/services/fs-watch/node';\n\nimport { parseAbsolute, type HostAbsolutePath } from '@emdash/core/primitives/path/api';\n```\n\nAvoid ambiguous module-root exports such as `@emdash/core/runtimes/git`.\nConsumers should choose `api`, `node`, or `browser` explicitly.\n\nInside `packages/core/src`, package subpath imports (the `imports` field in\n`package.json`) are used for cross-directory imports:\n\n```ts\nimport { parseAbsolute } from '#primitives/path/api';\nimport { createNativeWatchService } from '#services/fs-watch/node';\nimport { gitContract } from '#runtimes/git/api';\n```\n\nThose subpath imports are internal to Core source and resolve through Core's\nown `package.json`, so consumers need no extra configuration. Code outside\nCore should import through public `@emdash/core/...` subpaths.\n\n## More Details\n\nSee [`../../agents/architecture/core-modules.md`](../../agents/architecture/core-modules.md)\nfor the full architecture notes, dependency rules, and migration guidance.\n","isInternal":false,"tokens":1050,"sizeBytes":4916},{"name":"README.md","path":"packages/core/src/primitives/sqlite-store/README.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/packages/core/src/primitives/sqlite-store/README.md","title":"sqlite-store Documentation","category":"plugin-manifest","format":"markdown","content":"# SQLite stores\n\n`sqlite-store` standardizes how Emdash opens, migrates, tests, backs up, and\nrebuilds SQLite files. It deliberately does not depend on an ORM or a particular\nSQLite package. A host supplies a `SqliteDriver` and may attach an ORM through\n`createOrm`.\n\n## Choose a store type\n\n| Concern | Durable store | Derived store |\n| --- | --- | --- |\n| Intended data | User-authored or otherwise irreplaceable | Cache or index reproducible from another source |\n| Schema changes | Ordered migration history | Drop and rebuild |\n| Version state | `__emdash_migrations` plus runner metadata | Schema fingerprint in `PRAGMA user_version` |\n| Backups | Optional, before pending migrations | Not supported |\n| Downgrade behavior | Unknown newer migration rows are tolerated | Any version mismatch rebuilds |\n| Migration tests | `openAtMigration()` | Rebuild/idempotence tests |\n\nUse `defineDurableSqliteStore()` unless losing the whole database is always safe.\nA derived store's `createSchema` must be capable of recreating everything from\nscratch at any time.\n\n## Defining stores\n\n### Durable\n\nThe application owns the driver adapter and ORM attachment:\n\n```ts\nimport { defineDurableSqliteStore } from '@emdash/core/primitives/sqlite-store/node';\nimport { assertSqliteStoreInvariants } from '@emdash/core/primitives/sqlite-store/testing';\nimport { drizzle } from 'drizzle-orm/better-sqlite3';\nimport { betterSqlite3Driver } from './better-sqlite3-driver';\nimport { migrations } from './migrations.generated';\nimport * as schema from './schema';\n\nexport const appStore = defineDurableSqliteStore({\n  name: 'app',\n  driver: betterSqlite3Driver,\n  migrations,\n  createOrm: (connection) => drizzle(connection.native, { schema }),\n  backup: { retain: 2 },\n  invariants: [assertSqliteStoreInvariants],\n});\n\nconst handle = appStore.open('/path/to/emdash.db');\nhandle.db.select().from(schema.projects);\nhandle.close();\n```\n\n`createOrm` is optional. Without it, `handle.db` is the `SqliteConnection`.\n`postMigrate` hooks run with foreign keys enabled whenever the latest schema is\nopened. `invariants` run after those hooks when migrations were applied, for\nevery `openTemp()`, and after every `migrateToLatest()`. No-op production opens\nskip invariants so full database scans do not become a startup cost.\n\n### Derived\n\n```ts\nimport {\n  defineDerivedSqliteStore,\n  fingerprintDerivedSchema,\n  betterSqlite3Driver,\n} from '@emdash/core/primitives/sqlite-store/node';\n\nconst schemaSql = `\n  CREATE TABLE indexed_paths (\n    id INTEGER PRIMARY KEY,\n    path TEXT NOT NULL\n  ) STRICT\n`;\n\nexport const searchStore = defineDerivedSqliteStore({\n  name: 'file-search',\n  driver: betterSqlite3Driver,\n  version: fingerprintDerivedSchema(schemaSql),\n  createSchema(connection) {\n    connection.exec(schemaSql);\n  },\n});\n```\n\nWhen `user_version` is lower or higher than `version`, the primitive removes all\nnon-internal schema objects, calls `createSchema`, and stamps the requested\nversion in one transaction. Rebuilding a higher version is intentional: derived\nstores remain usable after an application downgrade.\n\n`fingerprintDerivedSchema()` converts the schema SQL itself into a positive\n31-bit `user_version`. It normalizes formatting whitespace outside quoted SQL\nvalues before computing SHA-256, so reformatting or changing line endings does\nnot rebuild production stores. Meaningful DDL and quoted-value changes do. Pass\nthe exact SQL constant executed by `createSchema`; when schema codegen is added,\ncompute the fingerprint from generator input before formatting generated\nTypeScript.\n\n#### Build-time Drizzle codegen\n\n`@emdash/core/primitives/sqlite-store/codegen` exposes\n`compileDrizzleSchemaToSql(schema)`. It uses `drizzle-kit/api` entirely in\nmemory to diff an empty SQLite snapshot against imported Drizzle table exports.\nThe helper sorts export keys before compilation so generated DDL is stable\nacross module loaders.\n\nKeep this import in build scripts and drift tests only. Runtime stores should\nimport a checked-in generated statement array, compute their fingerprint from\nthat array, and never bundle `drizzle-kit`.\n\n## Driver contract\n\n`SqliteConnection` exposes only:\n\n- `exec(sql)` for DDL and multi-statement SQL;\n- `get`, `all`, and `run` for prepared statements;\n- `close`;\n- `native` as an escape hatch for ORM attachment.\n\nThe store—not the driver—owns pragmas and transaction behavior. Every open sets\nWAL mode, `busy_timeout`, and foreign-key enforcement. Transactions use explicit\n`BEGIN IMMEDIATE`, `COMMIT`, and `ROLLBACK`, so different drivers cannot\nsilently change migration semantics. Transaction callbacks must be synchronous.\n\nUse `runSqliteDriverConformance()` from the testing entry point when adding a\ndriver. It verifies multi-statement execution, parameter binding, result shape,\nTEXT/INTEGER/BLOB/NULL round trips, exact large integers, runner pragmas,\nforeign-key toggling, transaction semantics, and `VACUUM INTO`.\n\n## Durable migrations\n\nA bundled migration contains stable identity and raw SQL:\n\n```ts\ntype BundledMigration = {\n  idx: number;\n  tag: string;\n  when: number; // legacy Drizzle journal milliseconds\n  hash: string; // SHA-256 of raw SQL file bytes\n  sql: string;\n};\n```\n\nThe runner:\n\n1. Bootstraps its bookkeeping schema when `user_version` is zero.\n2. Reads applied tags and verifies hashes for migrations known to this binary.\n3. Computes pending work by tag set difference, ordered by `idx`.\n4. Creates a backup when configured and a persistent database needs work.\n5. Disables foreign keys **before** opening migration transactions.\n6. Applies each migration atomically, splitting Drizzle SQL on\n   `--> statement-breakpoint`.\n7. Records `(tag, hash, applied_at)` in `__emdash_migrations`.\n8. Runs optional interop inside the same transaction.\n9. Runs `PRAGMA foreign_key_check`, then reenables enforcement in `finally`.\n\nRows for migrations unknown to an older binary are ignored. This allows additive\ndatabase upgrades to remain downgrade-tolerant. A known tag with a different\nhash throws because shipped migration SQL is immutable.\n\nDurable migration hashes are SHA-256 of the raw SQL file bytes. The algorithm\nis part of the runner schema contract rather than a per-row `hash_algo` column:\nchanging it requires a `RUNNER_SCHEMA_VERSION` bump that explicitly rewrites\nbookkeeping. Derived fingerprints need no stored algorithm because a changed\nimplementation causes one safe rebuild and then becomes self-consistent.\n\nSQLite ignores `PRAGMA foreign_keys` changes inside a transaction. Migration SQL\nmay contain those pragmas, but correctness comes from the outer runner protocol.\n\n### Legacy migration interop\n\nMigration history conversion is optional and pluggable:\n\n```ts\ntype MigrationInterop = {\n  backfill?(connection, migrations): void;\n  onApplied?(connection, migration): void;\n};\n```\n\n`backfill` runs once inside the bookkeeping bootstrap transaction. `onApplied`\nruns inside each migration transaction after the new bookkeeping row is\ninserted.\n\n`drizzleV0Interop` supports databases written by Drizzle's v0 journal layout. It:\n\n- reads `__drizzle_migrations`;\n- matches rows by `created_at === migration.when`, falling back to SQL hash;\n- throws rather than guessing if a row cannot be identified;\n- dual-writes newly applied migrations to `__drizzle_migrations`.\n\nDual-write keeps older application binaries from reapplying migrations after a\ndowngrade. Configure this interop only for stores with that legacy history.\n\n## Backups and restore\n\nDurable stores with `backup: { retain: N }` create consistent sibling backups\nusing `VACUUM INTO` before pending migrations. Paths are SQL-escaped, and old\nbackups are pruned by the creation timestamp embedded in their filename.\n\nCall `restoreLatestBackup(path)` only when every handle for that store path is\nclosed. Restore removes stale WAL/SHM sidecars and replaces the database through\na temporary file. The method refuses to run while the path is open.\nThat open-path guard is scoped to one store instance; callers must share the\ndefined store rather than independently defining stores for the same file.\n\n## Testing\n\n### Fully migrated temporary database\n\n`openTemp` creates an isolated on-disk database, applies all migrations, awaits\nthe optional seed, and deletes the database plus WAL/SHM files on close:\n\n```ts\nconst handle = await appStore.openTemp(async ({ db }) => {\n  await db.insert(projects).values({ id: 'project-1', name: 'Example' });\n});\n\ntry {\n  // test against handle.db or handle.connection\n} finally {\n  handle.close();\n}\n```\n\nIt is intentionally file-backed rather than `:memory:` so WAL, backup, and\nmulti-connection behavior stay representative.\n\n### Testing a specific migration\n\n`openAtMigration(n)` applies migrations whose `idx < n`. Seed pre-migration data\nwith raw SQL, because latest-schema ORM types cannot faithfully describe an old\nschema:\n\n```ts\nconst handle = appStore.openAtMigration(18);\ntry {\n  handle.connection.exec(`\n    INSERT INTO workspaces (id, type, ssh_connection_id)\n    VALUES ('workspace-1', 'project-ssh', 'missing-ssh')\n  `);\n\n  handle.migrateToLatest();\n  assertSqliteStoreInvariants(handle.connection);\n} finally {\n  handle.close();\n}\n```\n\nThis replaces opaque binary pre-migration fixtures with reviewable, adversarial\ntest setup. `openAtMigration` and backup/restore exist only on the durable store\ntype.\n\n`assertIncrementalMigrationEquivalence(store, migrationCount)` opens every\nhistorical migration boundary, migrates it to latest, and compares its\n`sqlite_schema` with a one-shot latest database. Use it as a compact regression\ntest for the complete migration chain.\n\n## Invariants\n\n- Never edit SQL for a shipped migration. Hash verification will reject it.\n- Hash raw UTF-8 file bytes without trimming or line-ending normalization.\n- Preserve legacy `when` milliseconds while `drizzleV0Interop` is needed.\n- `PRAGMA user_version` belongs exclusively to the store primitive: durable\n  stores use it for runner metadata; derived stores use it for a schema\n  fingerprint.\n- Domain code must not read or write `user_version`.\n- Derived data must be safe to delete at any time.\n- Store initialization after migration throws on integrity failures.\n  `assertSqliteIntegrity` runs SQLite's full B-tree and index consistency check;\n  `assertForeignKeyIntegrity` scans for orphaned references. This intentionally\n  differs from expected operational failures that use `Result`: callers cannot\n  safely continue with an unidentified or corrupt schema.\n","isInternal":false,"tokens":2325,"sizeBytes":10524},{"name":"README.md","path":"packages/core/src/runtimes/lsp/README.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/packages/core/src/runtimes/lsp/README.md","title":"lsp Documentation","category":"plugin-manifest","format":"markdown","content":"# LSP runtime\n\nBuffers use the host LSP runtime for hover, definition, type definition, references\nand diagnostics, according to each server's capabilities. `typescript-language-server`\n6.0.1 with TypeScript 6.0.3 and Pyright 1.1.414 ship with desktop and workspace-server.\nThey start only when needed; opening a Python file does not launch the TypeScript\nserver. Nothing is downloaded\nwhen a file opens. Other servers can be installed or selected in machine dependencies.\nRight-click a file tab and choose **Language services…** for status and restart;\nthere is no persistent language-service indicator in the file header.\nThe executable runs on the host containing the worktree, including SSH hosts.\nRemote language services are included in workspace protocol 12.0.\n\nThe LSP worker starts on the first language-service request in both desktop and workspace-server\nhosts. Its startup is excluded from required host readiness, so a failed or pending LSP startup does\nnot block unrelated runtimes. A later request can retry failed startup; concurrent requests share\none attempt, and successful readiness stays cached. Individual language servers still start only\nwhen a session is attached.\n\n| Languages | Server | Host requirements |\n| --- | --- | --- |\n| TypeScript, JavaScript, JSX, TSX | [typescript-language-server](https://github.com/typescript-language-server/typescript-language-server) with TypeScript 6 | Bundled, runs with Emdash’s Node/Electron; no external Node required |\n| Bash, sh | [bash-language-server](https://github.com/bash-lsp/bash-language-server) | Node.js 20+; ShellCheck on PATH adds lint diagnostics |\n| Go | [gopls](https://go.dev/gopls/) | Go toolchain; make GOBIN or GOPATH/bin available on PATH after installing |\n| Rust | [rust-analyzer](https://rust-analyzer.github.io/book/installation.html) | Rust toolchain, Cargo and rust-src; the offered rustup command installs the analyzer and sources |\n| Python | [Pyright](https://github.com/microsoft/pyright) | Bundled, runs with Emdash’s Node/Electron; uses the project’s Python environment |\n| C, C++ | [clangd](https://clangd.llvm.org/installation) | Project compiler flags, normally compile_commands.json; Homebrew LLVM's bin directory must be on PATH or selected explicitly |\n| JSON, JSONC | [VS Code JSON server](https://github.com/hrsh7th/vscode-langservers-extracted) | Node.js; JSONC includes .jsonc, tsconfig.json and jsconfig.json |\n| YAML | [yaml-language-server](https://github.com/redhat-developer/yaml-language-server) | Node.js; schemas may be associated using a file modeline |\n| HTML, CSS, SCSS, Less | [VS Code HTML/CSS servers](https://github.com/hrsh7th/vscode-langservers-extracted) | Node.js; HTML, CSS and JSON executables share one npm install package |\n\nBash selection includes `.sh`, `.bash` and known startup files such as `.bashrc`;\nthere is no shebang discovery for arbitrary extensionless scripts. Zsh and Fish\ndo not activate the Bash server. Navigation capabilities vary: for example, schema\nlanguages principally offer hover and validation, not programming-language type\ndefinitions. Monaco retains its existing completion and formatting providers;\nhost-backed LSP completion, formatting and workspace edits are not implemented.\n\nHover and navigation select a provider per document and capability. Missing tools,\nstarting/failed servers, unsupported capabilities and disconnected hosts fall back\nto Monaco's bundled services where available (TS/JS, CSS/SCSS/Less, HTML and JSON).\nA successful empty host answer is authoritative. Local TypeScript validation keeps\nthe editor's existing syntax-only policy. There is one diagnostic marker owner per\nmodel: current host publications, including empty ones, supersede local validation;\nedits clear stale diagnostics, and loss of the host restores local validation. Late\nresults cannot replace a newer document or provider's markers. Disk/Git snapshots\nretain local services and are never replicated to a host.\n\nFallback analyzes loaded models and bundled declarations, not the project's filesystem\nor full configuration. It restores basic hover, definition/reference navigation and\nvalidation where Monaco supports them; type-definition navigation still requires host\nLSP. Rust, Go and other languages without Monaco services still need their host server.\nThe language-services dialog explains basic support and offers host-server retry.\n\nThe Monaco adapter owns provider selection and model lifetimes. Local worker access\nand diagnostic arbitration are separate editor modules, with no host or Wire\nknowledge. Automatic registrations for overlapping Monaco features are disabled;\nthe editor invokes the local workers as fallback. TypeScript reuses Monaco's worker;\nCSS, HTML and JSON fallback workers start lazily and are released after two minutes\nof inactivity. Host availability never changes global language defaults.\n\n[editor/browser/lsp/monaco-language-services.ts](../../../../../apps/emdash-desktop/src/core/features/editor/browser/lsp/monaco-language-services.ts) adapts Monaco models, providers,\ndiagnostics and navigation. It registers providers once during lazy Monaco bootstrap.\n[editor/api/browser/lsp/language-service-client.ts](../../../../../apps/emdash-desktop/src/core/features/editor/api/browser/lsp/language-service-client.ts) owns project discovery, session\nsharing, buffer lifetime, cancellation and synchronization before queries. Its document\nbindings expose semantic queries without requiring Monaco or session keys. File tabs\nregister the originating task and workspace root before their buffer models are created. Only `emdash-buffer:` models are\nreplicated; disk and Git snapshots never enter the language server. Shared model\nlifetime owns open/close, so split panes and dirty buffers surviving tab closure\nremain consistent. `DocumentSynchronizer` coalesces edits and flushes every open\nbuffer before a query. Unchanged versions are checked before reading model text.\nAfter the initial snapshot, the synchronizer sends one compact UTF-16 edit against\nthe last acknowledged version. A mismatched base requests a fresh snapshot;\nreconnects also replay snapshots. The host retains complete text for restart and\nexpands edits only when a server requires full-document synchronization. The shared\nfile store emits successful saves with the exact text written to disk. That immutable\nsnapshot crosses Wire with the save notification; the host uses it for didSave's\noptional text instead of its newer live overlay. Synchronization can keep newer edits\nwithout rolling the server document back. The store leaves those edits dirty and\nrecoverable, and isolates synchronous and asynchronous save-observer failures from\nthe completed write. Results preserve host identity and the originating task when\nnavigating outside the root.\n\n`LanguageSessionClient` leases typed live state through the editor Wire domain.\nIts `attachedClient` promise means the Wire attachment exists; server readiness is\na separate live state. Connection state is separate from authoritative server status.\nIndividual request failures are reported to the caller without marking the server\nas failed or clearing diagnostics. Cancelled and stale queries are discarded. The\nNode editor controller routes to `RuntimeBroker`; it does not own processes or\nLSP protocol state. `packages/core/src/runtimes/lsp/` owns process framing,\ninitialization, document versions, cancellation, capabilities, diagnostics and\nshutdown. It runs as a worker on both desktop and workspace-server, with one\nserver per renderer client, resolved project root and server ID. Server commands come\nfrom bundled assets or host dependency descriptors, never renderer-supplied shell text. A live-state\nlease keeps the server alive; the last detach releases it after a short grace\nperiod. Explicit restart replays unsaved buffers, and replacement generations\ninvalidate renderer synchronization caches.\n\nThe server registry uses UTF-16 positions and a two-million-character document limit. Adding\nanother server means adding portable selection metadata in [lsp/api/server-catalog.ts](api/server-catalog.ts)\nand a host profile in [lsp/node/server-registry.ts](node/server-registry.ts), then exercising its capability\nnegotiation and synchronization behavior. The renderer derives provider selectors,\nprotocol language IDs, session keys and labels from that metadata. Monaco-specific\nlanguage mappings remain in the editor adapter; Core contains only protocol language\nIDs. Host profiles own executable descriptors, arguments, project-root markers and configuration;\nthe process transport only binds their supplied request handlers to JSON-RPC.\n\nProject discovery runs through Wire on the file's host before acquiring a session.\nFor TS/JS, the nearest `tsconfig.json`, `jsconfig.json` or `package.json` between the\nfile and task workspace selects the root; absent markers and external definition\ntargets retain the task workspace root. Automatic selection uses the bundled\n`typescript-language-server`. The TypeScript profile resolves the project's\n`tsserver.js`, including hoisted dependencies, and falls back to the bundled TS6\ncompiler if the project has no compatible server. Resolution does not execute\nproject code. Explicit machine-dependency selections override the bundled launch;\ninvalid selections remain visible as errors. Automatic typing acquisition is\ndisabled so opening a JavaScript file never starts npm downloads. Go recognizes\n`go.mod`/`go.work`; Rust recognizes `Cargo.toml` and\n`rust-project.json`; Python recognizes Pyright, pyproject and common Python project\nfiles; C/C++ recognizes compilation databases, `.clangd` and CMake roots. Each uses\nthe nearest marker inside the task workspace. Bash, JSON, YAML, HTML and CSS use\nthe task workspace root without borrowing a nearby TypeScript package root.\n\nThe Python host profile discovers `.venv`, then `venv`, then a captured `VIRTUAL_ENV`\nwith an executable interpreter, without running project code during discovery.\nOtherwise Pyright discovers Python from the host environment. Pyright continues\nto read `pyrightconfig.json` and `pyproject.toml`; diagnostics are limited to open\nfiles. Go and Rust inherit the captured host toolchain environment. The process\ntransport remains independent of these policies.\nHost configuration is resolved again when a server restarts.\n`vscode-languageserver-protocol` (MIT) supplies JSON-RPC framing and cancellation.\nTypeScript 6.0.3 and `typescript-language-server` 6.0.1 (both Apache-2.0) are pinned\nproduction dependencies, alongside Pyright (MIT). Both servers use the host's\nEmdash Node/Electron executable; Electron launches them with `ELECTRON_RUN_AS_NODE`.\nThe same JavaScript assets work on every supported CPU architecture. Electron\nunpacks the servers and their runtime data, with an explicit resource-copy rule\npreserving TypeScript's `.d.ts` standard libraries, which its default module\nfilter would otherwise remove. Workspace-server stages the same pinned versions.\nRelease validation rejects missing assets or accidental resolution outside the\nproduction deployment. The repository's native-preview compiler remains a\ndevelopment build tool and is not a bundled language server.\n\nAfter Electron finishes packaging, native-architecture release builds also launch\nboth servers using the finished app's executable with an empty PATH and isolated\nhome. The check follows definitions into the shipped TypeScript declarations and\nPyright stubs and verifies type diagnostics. Cross-compiled apps need the same check\non a matching host via `scripts/release/verify-language-servers.ts --executable <binary>\n--resources <resources-directory>`. It does not modify the app or launch its UI.\nThe dedicated CI packaging job exercises stable and canary Electron packages and\nverifies that removing standard-library assets makes the check fail.\nThe probe reuses the existing MIT-licensed LSP protocol client as a direct desktop\ndevelopment dependency instead of implementing a second JSON-RPC transport.\n\nBash, YAML and `vscode-langservers-extracted` remain development-only integration\nfixtures. The extracted web-server package brings TypeScript 4.9.5 transitively;\nit is not shipped as our TypeScript server or as a production dependency. Monaco\nsupplies basic HTML/CSS/JSON support without adding that aggregate server package.\n\nThe Wire contract names snapshot replacement (`setDocumentSnapshot`), versioned\nedits (`applyDocumentEdit`), save notification (`documentSaved`) and process recovery\n(`restartServer`) explicitly. Definition, type-definition and reference queries are\nseparate operations; references preserve the caller's `includeDeclaration` option.\nSnapshot replication is required for initialization and recovery, not compatibility.\nThis unreleased feature is included in protocol 12.0, with no legacy LSP aliases.\n\nTests cover protocol state and actual server behavior in Core, host routing and\nbuffer policy in desktop Node tests, and Monaco providers/navigation in browser\ntests. [language-services.e2e.browser.test.ts](../../../../../apps/emdash-desktop/src/core/features/editor/browser/lsp/language-services.e2e.browser.test.ts) carries actual Wire frames through\nPlaywright bindings into the production editor controller and host runtime,\nthen checks cross-file unsaved types, definitions, diagnostics and restart with\nreal TypeScript and Python servers. The bindings exist only in Vitest's test harness.\nCore's [language-servers.integration.test.ts](node/language-servers.integration.test.ts) additionally checks production host\nprofiles against real Bash, JSON/JSONC, YAML, HTML, CSS/SCSS/Less servers in ordinary\ntest runs. Enable its native Go, Rust and C++ cases with installed `gopls`, `go`,\n`rust-analyzer`, `cargo`, `rustc` and `clangd` on PATH:\n\n```bash\nEMDASH_TEST_NATIVE_LSP=1 pnpm --dir packages/core exec vitest run src/runtimes/lsp/node/language-servers.integration.test.ts\n```\n\nThis opt-in fails if a requested tool is absent; it does not install tools or silently\nskip a broken setup. It covers navigation, type definitions where supported, unsaved\nedits, restart replay and lease cleanup. Schema and stylesheet cases also verify\ndiagnostics arriving and clearing after unsaved edits.\n","isInternal":false,"tokens":2872,"sizeBytes":14257},{"name":"README.md","path":"packages/shared/README.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/packages/shared/README.md","title":"shared Documentation","category":"plugin-manifest","format":"markdown","content":"# @emdash/shared\n\n`@emdash/shared` is the repo's standard library: package-level foundations reused\nby Wire, Core, desktop, workspace-server, and tests. These primitives are\nintentionally generic — they do not define product domains, Wire protocol\nmessages, or host-specific process behavior.\n\n## Standard-Library Stance\n\nWorking primitives keep their exports regardless of measured consumers. A\n`Result` type without `map`/`andThen` is half a type, so the full combinator\nAPI stays exported even when a usage census finds few callers; the same goes\nfor the request middleware family, `createDurableQueue`,\n`createSharedResource`, and the logger utilities. Do not re-litigate\nzero-consumer exports in the next census. What this stance does **not** cover\nis genuine machinery: internals such as timer-delay normalization stay\nunexported.\n\n## The Prelude\n\nThe root entrypoint (`@emdash/shared`) is the single home for the blessed\ncross-cutting core:\n\n- The result module: `Result<T, E>`, `ok`/`err`/`fail`, guards, combinators\n  (`map`, `andThen`, `tryCatch`, `gen`, …), and the serialization/error family\n  (`Serializable`, `SerializedError`, `BaseError`, `toSerializedError`,\n  `resultSchema`).\n- `Unsubscribe` and the lifecycle leases: `Lease`, `PendingLease`,\n  `toPendingLease`, `once`.\n- `createEmitter` and the `Emitter<T>` interface.\n- `isDeepEqual`.\n- `Secret`: `secret`, `isSecret`, `reveal`, `REDACTED`.\n\nEvery symbol has exactly one home package-wide: prelude symbols come from the\nroot, domain symbols from their subpath. Nothing is exported from both.\n\n## Entrypoints\n\n- `@emdash/shared` — the prelude (above).\n- `@emdash/shared/concurrency`: `Scope` (`createScope`), `createLifecycleRegistry`,\n  `createConcurrencyLimiter`, `createKeyedLanes`, `createMailbox`,\n  `createAsyncCache`, `createResourceCache`, `createSharedResource`,\n  `createDurableQueue`, `createBoundedBuffer`, and `Disposable`.\n- `@emdash/shared/scheduling`: `Clock`, `systemClock`, `abortableWait`,\n  `abortReason`, `throwIfAborted`, `waitWithSignal`, `retry`, `retrySchedule`\n  and the `retrySchedules` combinators, `runWithTimeout`, `TimeoutError`, and\n  `TimerHandle`.\n- `@emdash/shared/requests`: request orchestration — `compose` middleware,\n  `withRetry`, `withTimeout`, `withScheduler`, `deduplicate`,\n  `createRequestScheduler`, and `createTokenBucketGate`.\n- `@emdash/shared/logger`: the browser-safe logging surface — `log`,\n  `runWithLogger`, levels, formatting, field preparation, and redaction.\n- `@emdash/shared/logger/node`: the Node-only surface — `initProcessLogging`,\n  `createPinoLogger`, `createFileTransport`, and `installAsyncLogContext`.\n- `@emdash/shared/plugins`: the plugin framework — capabilities, assets,\n  registries, and `PluginIconAsset` (the one icon-asset type; never re-alias it).\n- `@emdash/shared/testing`: `deferred`, `createManualClock`,\n  `createStubLogger`, and `waitFor`.\n- `@emdash/shared/util`: stable utility helpers such as `stableStringify`.\n- `@emdash/shared/markdown`: mention grammar helpers.\n- `@emdash/shared/config`: layered config parsing with zod schemas.\n- `@emdash/shared/perf` and `@emdash/shared/perf/node`: spawn accounting and\n  process vitals instrumentation.\n\n## Choosing Lifecycle Primitives\n\nUse `Scope` for ownership, cancellation, cleanup ordering, and async work that\nmust not outlive its feature. Use `scope.add()` and `scope.use()` for finalizers,\n`scope.child()` for nested ownership, and `scope.run()` for tracked async work.\n\nUse `createLifecycleRegistry` when a feature owns a keyed set of local resources\nwith explicit `start()` and `stop()` commands, typed start/stop results,\nqueryable state, and state-change observers. It is a state registry, not a lease\ncache or protocol primitive.\n\nUse `createResourceCache` when resource lifetime is demand-driven: consumers call\n`acquire()`, hold leases, release them, and optionally benefit from an idle TTL.\nUse `createSharedResource` for the same lease behavior around one unkeyed\nresource, and `createAsyncCache` for retryable cached async values with no\nfinalizer.\n\nThe command/event/effect machine primitive is internal to `@emdash/core`'s acp\nruntime (`packages/core/src/runtimes/acp/node/machine/primitive/`) and is not\noffered as a shared building block. Use Wire-owned primitives when the lifecycle is\nprotocol-specific: Wire workers belong to `@emdash/wire/worker` because they\nsupervise process generations and keep a stable typed Wire client, and\n`LiveJobSource` belongs to `@emdash/wire/live` because it publishes cancellable\njob state, progress, retention, and remote client handles.\n\n## Package Conventions\n\n- **Ownership-drop**: primitives that take ownership of values fire `onDrop`\n  exactly once for every taken-then-discarded value, and never reject because\n  of it.\n- **Never-silent**: where a failure would otherwise vanish, optional failure\n  hooks default to logger-backed reporting rather than swallowing (for example\n  the emitter's `onSubscriberError` and keyed-lane coalesce failures fall back\n  to `log.warn`).\n- **Clock seam**: no raw `Date.now`/`setTimeout` outside `scheduling/clock.ts`;\n  time-dependent primitives accept a `Clock`.\n","isInternal":false,"tokens":1261,"sizeBytes":5179},{"name":"README.md","path":"packages/wire/docs/README.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/packages/wire/docs/README.md","title":"docs Documentation","category":"plugin-manifest","format":"markdown","content":"# @emdash/wire Docs\n\n`@emdash/wire` is the transport-agnostic runtime layer for typed API calls,\nlive model subscriptions, live logs, event streams, jobs, mutations, workers, and\nthe small utilities that sit at the API boundary.\n\nWire builds on Shared foundations for generic lifecycle, scheduling,\nconcurrency, testing, and stable utility behavior:\n\n```mermaid\nflowchart TB\n  subgraph shared [Shared foundations used by Wire]\n    scope[Scope and ResourceCache]\n    registry[LifecycleRegistry]\n    scheduling[Clock and RetrySchedule]\n    mailbox[Mailbox]\n  end\n  subgraph api [API layer]\n    contracts[defineContract endpoint kinds]\n    controller[createController]\n    client[contract client]\n    transports[Transports: memory, port, dom-port, electron, stream, reconnecting]\n  end\n  subgraph live [Live primitives]\n    model[LiveStateSource and replicas]\n    log[LiveLogSource]\n    eventStream[EventStream]\n    job[LiveJobSource]\n    mutations[Mutations and registries]\n  end\n  subgraph workers [Wire workers]\n    component[WireComponent]\n    workerHost[WireWorkerHost]\n  end\n  shared --> api --> live\n  component --> api\n  workerHost --> api\n  workerHost --> component\n```\n\nThe live layer owns the stateful primitives: `LiveStateSource`, `LiveLogSource`,\n`EventStreamSource`, `LiveJobSource`, and consumer-instantiated replicas; live\nmodels are served by providers, usually `expose()` over kernel state. Low-level `*Client` followers track cursors and resync, while\nmaterializers (`StateStore`, `LogSink`, `JobStore`) own values. Most consumers use\nclient handles directly or wrap them in replicas. The API layer turns those\nprimitives into a contract with typed procedure calls and live topic client\nhandles. `WireComponent` is the reusable contract implementation pattern: it\ndeclares explicit typed requirements, validates config at the creation boundary,\nand can be created in-process or hosted in a worker. Worker hosting is Wire-specific because it\nserves components across processes. Generic lifecycle, scheduling, concurrency, testing, and stable\nutility primitives live in `@emdash/shared` and are documented here where Wire\nuses them. The `WireInstrumentation` seam is cross-cutting and can be attached\nto API, live, and worker surfaces through their options.\n\n## Pages\n\n- API:\n  - [Contracts](./api/contracts.md): `defineContract()`, endpoint kinds, nested\n    composition, and live model groups.\n  - [Serving and clients](./api/serving.md): `createController()`, `serve()`,\n    `connect()`, cancellation, controller composition, session hubs, and\n    server-side call helpers.\n  - [Composable middleware](./api/middleware.md): target-first `compose()`,\n    handler middleware, controller middleware, timeout, retry, and\n    deduplication.\n  - [Typed clients](./api/clients.md): `ContractClient` handles,\n    `forwardController()`, and selective forwarding through `createController()`.\n  - [File endpoints](./api/files.md): `downloadFile()`, `uploadFile()`, blob\n    channels, and binary stream transport framing.\n  - [Wire errors](./api/errors.md): error planes, `WireErrorCode` meanings,\n    origins, and retry guidance.\n  - [Transports](./api/transports.md): memory, ports, Electron, streams, and\n    reconnecting transports.\n- Live:\n  - [Live models and protocol](./live/live-model.md): snapshots, updates,\n    cursors, `LiveStateSource`, and replicas.\n  - [Live logs](./live/live-log.md): retained terminal-style logs and client\n    callbacks.\n  - [Event streams](./live/event-stream.md): keyed fire-and-forget events with\n    explicit gap callbacks after reattach.\n  - [Live jobs](./live/live-job.md): progress, cancellation, terminal state,\n    retention, and contract job handles.\n  - [Mutations](./live/mutations.md): mutation ids, host contexts, cursor settling,\n    idempotency cache, and retry behavior.\n  - [Replicas](./live/replicas.md): `LiveModelReplicaCache`, `LiveLogReplicaCache`,\n    `LiveJobReplicaCache`, pluggable stores, ref counting, and serving cached state.\n- Runtime:\n  - [Lifecycle utilities](./runtime/lifecycle.md): Shared `Scope`,\n    `LifecycleRegistry`, scope loggers, `describeScope()`, and resource\n    ownership.\n  - [Structured concurrency](./runtime/structured-concurrency.md): `Scope.run()`,\n    run cancellation, lifecycle invariants, and diagnostics.\n  - [Scheduling](./runtime/scheduling.md): Shared `Clock`, `TimerHandle`,\n    `ManualClock`, retry schedules, abortable sleeps, and timer ownership.\n  - [Resource caches](./runtime/resource-cache.md): Shared `ResourceCache`,\n    `SharedResource`, `AsyncCache`, identity rules, and lease behavior.\n  - [Mailbox and Broadcast](./runtime/mailbox-and-broadcast.md): Shared bounded\n    local async handoff, overflow policy, guarantees, and the deferred Broadcast\n    contract.\n  - [Components](./runtime/components.md): `defineWireComponent()`, explicit\n    requirements, in-process creation, worker deployment, and non-DI composition\n    rules.\n  - [Workers](./runtime/workers.md): `WireWorkerHost`, one-generation spawners,\n    `runWireComponentWorker()`, and process-hosted components.\n- [Observability](./observability.md): ambient logger context, the\n  `WireInstrumentation` seam, and scope loggers.\n\nRunnable examples live under [../examples](../examples). Most snippets in these\ndocs are shortened versions of those files.\n\n## Package Exports\n\nThere is no root export: every symbol has exactly one home in a hand-curated\nsubpath entrypoint.\n\n```ts\nimport { createController, defineContract } from '@emdash/wire/rpc';\nimport { cell, expose } from '@emdash/wire/state';\n```\n\n- `@emdash/wire/rpc`: contract definition (`defineContract()`, endpoint\n  factories), controller creation, clients, `connect()`/`serve()`, transports,\n  protocol vocabulary (`WireMessage`, `WireTransport`, `WireError`,\n  `LiveSource`, `LiveUpdate`, cursors), provider seam types\n  (`LiveModelProvider`, `LeasedLiveModelProvider`,\n  `LiveModelMutationEnvelope`), the blob/file surface, validation, and the\n  `WireInstrumentation` seam types. Retry schedules come from\n  `@emdash/shared/scheduling`.\n- `@emdash/wire/live`: the server-side reactivity sources (`LiveLogSource`,\n  `LiveJobSource`, `EventStreamSource`, `createEventStreamHost()`), the keyed\n  replica caches (`createLiveLogReplicaCache()`, `createLiveJobReplicaCache()`,\n  `ReplicaLog`), and resync failure policies (`resyncRetry()`,\n  `resyncMarkStale()`).\n- `@emdash/wire/state`: state kernel primitives and Wire bridges (`cell`,\n  `query`, `family`, `optimistic`, `observe`, `expose`, `remote`).\n- `@emdash/wire/mobx`: MobX-backed log/store helpers\n  (`createImmutableMobxStore`, `createReactiveMobxStore`, `createMobxLogStore`).\n- `@emdash/wire/testing`: Wire test helpers such as `createTestWire()` and fake\n  worker process support.\n- `@emdash/wire/worker`: `WireWorkerHost`, `runWireComponentWorker()`,\n  `defineWireComponent()` and component requirement helpers, worker signal\n  types, supervision types, process contracts, and worker log forwarding.\n- `@emdash/wire/worker/node`: Node `childProcessSpawner()`.\n\nUse Shared subpaths directly for generic foundations:\n\n- `@emdash/shared/concurrency`: `Scope`, `Run`, `LifecycleRegistry`, `Mailbox`,\n  `ResourceCache`, `SharedResource`, `AsyncCache`, bounded buffers, and disposable\n  helpers.\n- `@emdash/shared/scheduling`: `Clock`, `systemClock`, `TimerHandle`,\n  `TimeoutError`, `runWithTimeout()`, `RetrySchedule`, retry schedule builders,\n  and `retry()`.\n- `@emdash/shared/requests`: request handler composition, timeout, retry,\n  in-flight deduplication, and request scheduling.\n- `@emdash/shared/testing`: `createManualClock()`, `deferred()`, `waitFor()`, and\n  stub logger helpers.\n- `@emdash/shared/util`: stable utility helpers such as `stableStringify()`.\n\nMobX-backed utilities intentionally live in their own export because they have a\n`mobx` peer dependency. Server-only code can import `@emdash/wire/rpc`,\n`@emdash/wire/live`, `@emdash/wire/state`, `@emdash/wire/worker`, and Shared\nfoundation subpaths without pulling in MobX.\n\n## Typical Flow\n\n1. Define a contract with `defineContract({ ... })`.\n2. Create server-side `cell`, `query`, `LiveLogSource`, `EventStreamSource`, or `LiveJobSource`\n   instances and publish live models with `expose()`.\n3. Use `family()` or explicit domain indexes for keyed live model resources.\n4. Create a controller with `createController(contract, impl)`. Validation is\n   applied by default (`'full'` outside production, `'inputs'` in production);\n   pass `{ validate }` to override.\n5. Serve the controller over a `WireTransport`.\n6. Connect from the client and create a typed `client()`.\n7. Use client handles directly for streaming, or use `remote()` for local state,\n   ref counting, and downstream observation.\n\nFor a complete state kernel example (`cell` + `expose` on the server,\n`remote()` on the client), see [../examples/state-kernel](../examples/state-kernel).\n\n## Event Stream Attachment Law\n\nEvent streams retain no events, and a connection deduplicates attachments to the same topic.\nAnything a late attacher must learn therefore has to live in a retained snapshot or in the attach\nacknowledgment, never in an event-stream message. A one-shot `ready` event is not a valid readiness\nbarrier: a consumer joining an already-attached topic will not cause the server to emit it again.\n\nUse `resourcedStream()` when a successful attach must guarantee that an underlying resource is\nready. Its host must be created with `createEventStreamHost(def, { activate })`, where `activate`\nreceives an `AbortSignal` and resolves to the activation-owned disposer only after the resource is\nready. The source registers the subscriber before activation begins, shares and retains the\nactivation promise for all subscribers to the key, and invokes the disposer when the final\nsubscriber leaves. If the final subscriber leaves while activation is pending, the signal aborts\nand any late result is disposed. Failed activation is not retained, so a later attach makes a fresh\nattempt.\n\nThis guarantee is enforced when a controller binds the endpoint: a resourced definition accepts\nits matching host or a forwarded client handle, but rejects a bare resolver. Forwarding preserves\nthe barrier because the downstream attach is acknowledged only after the upstream attach is\nacknowledged.\n","isInternal":false,"tokens":2426,"sizeBytes":10327},{"name":"README.md","path":"packages/wire/examples/README.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/packages/wire/examples/README.md","title":"examples Documentation","category":"plugin-manifest","format":"markdown","content":"# @emdash/wire Examples\n\nThese examples show the public surface of `@emdash/wire`. Every example imports\nthe package entrypoints only (`/rpc`, `/live`, `/state`, via their in-repo\nsource paths) and splits the authoritative server side from the client-side\nbinding so the boundary looks like a real transport without adding an adapter.\n\n## Documentation\n\nFor conceptual docs that explain how the examples fit together, see\n[../docs/README.md](../docs/README.md).\n\nRun them from the repository root:\n\n```bash\npnpm --filter @emdash/wire run example:state-kernel\npnpm --filter @emdash/wire run example:live-log\npnpm --filter @emdash/wire run example:event-stream\npnpm --filter @emdash/wire run example:mailbox\npnpm --filter @emdash/wire run example:cancellation\npnpm --filter @emdash/wire run example:api-definition\npnpm --filter @emdash/wire run example:dedupe\npnpm --filter @emdash/wire run example:job-contract\n```\n\nExamples:\n\n- `state-kernel/` demonstrates the state kernel end to end: `cell` + `family`\n  + `expose` on the server, `remote()` + `observe` + `optimistic` on the\n  client, including an optimistic mutation settling against the authoritative\n  cursor.\n- `live-log/` demonstrates a contract-level `liveLog()` endpoint served by a\n  `LiveLogSource`, consumed through `createLiveLogReplicaCache` and\n  `ReplicaLog`.\n- `event-stream/` demonstrates a contract-level `eventStream()` endpoint served\n  by `createEventStreamHost` with client-side subscribe and gap handling.\n- `mailbox/` demonstrates bounded local producer/consumer handoff, graceful close,\n  and explicit overflow.\n- `cancellation/` demonstrates procedure cancellation with `AbortSignal` and\n  server-side abort on disconnect.\n- `api-definition/` isolates contract definition with `defineContract`,\n  `procedure`, `liveModel`, `liveLog`, and live model contract\n  mutations.\n- `dedupe/` demonstrates server-side `deduplicate()` middleware from\n  `@emdash/shared/requests` for in-flight procedure calls.\n- `job-contract/` demonstrates the contract-level `liveJob()` endpoint with start,\n  progress, cancellation, terminal result, and reattach.\n- `scope/` and `resource-cache/` demonstrate the `@emdash/shared/concurrency`\n  primitives the live layer builds on.\n","isInternal":false,"tokens":546,"sizeBytes":2233},{"name":"README.md","path":"packages/wire/src/state/README.md","rawUrl":"https://raw.githubusercontent.com/generalaction/emdash/HEAD/packages/wire/src/state/README.md","title":"state Documentation","category":"plugin-manifest","format":"markdown","content":"# `@emdash/wire` reactive state (`wire/src/state/`)\n\n> **Status: implemented in parallel to `wire/src/live/`.** The primitives in\n> this folder are available as `@emdash/wire/state` and are in production use:\n> desktop feature slices (tasks, workspaces, source-control, editor, projects)\n> author providers with `expose` and consume them with `remote`. See\n> [05-migration.md](./05-migration.md) for the remaining migration plan.\n\nThis folder holds the reactive state kernel for wire: a small set of\nsignals-style primitives that unify how authoritative state is produced,\nderived, and published over the wire — and how replicas of it are consumed on\nthe other side.\n\nIt replaces the *value-reactivity* portion of `wire/src/live/` (the four ad-hoc\n\"a value that changes\" flavors: `LiveStateSource` used directly by features,\n`ComputedLiveState`, deleted `BatchedLiveState`, `bindMachineToLiveState`) and the\nhand-written provider/lease boilerplate around them. It does **not** replace\nthe wire protocol (`live/protocol/`), the replica transport, live logs, event\nstreams, or live jobs — those stay. See\n[04-wire-integration.md](./04-wire-integration.md) for the exact boundary.\n\n## Why\n\nToday every stateful runtime hand-rolls the same machinery: dirty flags,\ndebounce timers, one-in-flight-plus-one-queued refresh collapse, lease\nlifecycles, imperative invalidation fan-out, and per-provider\n`acquireState`/`ready`/`release` plumbing. The former file `TreeResource` spent\n~200 lines on scheduling; `RepositoryResource` manually fans invalidations out to every\ncheckout; every `LeasedLiveModelProvider` implementation repeats the same ~40\nlines. The kernel moves all of that into one tested core and leaves feature\ncode with declarations:\n\n```ts\n// A keyed, DB-backed live query — the whole provider.\nconst taskList = family((key: { projectId: string }, scope) =>\n  query({\n    fetch: () => selectTaskListView(db, key),\n    pokes: [tasksChannel.match(key), conversationsChannel.match(key)],\n    scope,\n  }));\n\nexpose(tasksContract.taskList, taskList);\n```\n\n## The primitives at a glance\n\n| Primitive | One-liner | Doc |\n|---|---|---|\n| `cell` | Writable value; the leaf for in-memory sources | [02](./02-primitives.md#cell) |\n| `derived` | Pure, auto-tracked computation over other reactives | [02](./02-primitives.md#derived) |\n| `query` | Async computed from an *external* source (DB, git, fs); declared invalidation via pokes; write-through via `settle` | [02](./02-primitives.md#query) |\n| `family` | Keyed instantiation of any reactive, refcounted with linger GC | [02](./02-primitives.md#family) |\n| `observe` | The only side-effect edge: subscribe a callback, owned by a `Scope` | [02](./02-primitives.md#observe) |\n| `peek` | Untracked read | [02](./02-primitives.md#peek-and-untracked-reads) |\n| `batch` | Group synchronous writes into one turn with shared metadata | [02](./02-primitives.md#batch) |\n| `pokeChannel` | Named invalidation channel connecting external change signals to queries | [02](./02-primitives.md#pokechannel) |\n| `optimistic` | Pending-edit overlay over an authoritative base: apply-now, ack-prune, rollback-for-free | [02](./02-primitives.md#optimistic) |\n| `pin` / `prefetch` | Explicit demand at lifecycle boundaries: fetch at mount/intent, not at render | [02](./02-primitives.md#pin-and-prefetch) |\n| `expose` | Publish a reactive/family as a wire live model (generates the provider) | [04](./04-wire-integration.md#expose) |\n| `remote` | Consume a wire live model as a reactive family (wraps the replica) | [04](./04-wire-integration.md#remote) |\n\nOwnership and disposal always flow through `Scope` from\n`@emdash/shared/concurrency` — there is no manual `release()` in the common\npath. On the client side, demand is driven by `pin`s owned by lifecycle\nboundaries (view scopes), never by render-time observability — see\n[01-concepts.md §6](./01-concepts.md#demand-is-a-policy-not-a-rendering-accident).\n\n## Testing\n\nThe state module is tested in four deterministic tiers:\n\n- Kernel tests in `core/kernel.test.ts` and `core/family.test.ts` cover turns,\n  dynamic dependencies, error recovery, observer isolation, mutation-id\n  folding, and linger GC.\n- Primitive tests in `query.test.ts`, `optimistic.test.ts`, and `pin.test.ts`\n  cover async races, write-through, TTL rollback, generation pruning, and\n  demand aggregation.\n- Bridge tests in `bridge/expose.test.ts` cover readiness, leases, and\n  derived-hop mutation observation.\n- `bridge/roundtrip.test.ts` runs `expose -> wire transport -> remote ->\n  optimistic` over the real test wire harness to protect read-your-writes and\n  no-flicker behavior end to end.\n\n## Reading order\n\n1. [01-concepts.md](./01-concepts.md) — the mental model: nodes, turns,\n   observation, statuses, the tracked/declared boundary, publish metadata.\n2. [02-primitives.md](./02-primitives.md) — reference for each primitive:\n   signatures, semantics, options, edge cases.\n3. [03-composition.md](./03-composition.md) — how primitives compose; worked\n   examples (git runtime, file tree, DB-backed lists, optimistic overlays);\n   the granularity rule that keeps performance flat.\n4. [04-wire-integration.md](./04-wire-integration.md) — `expose`/`remote`,\n   mutations, read-your-writes cursors, and the exact mapping onto the\n   existing `live/protocol/` machinery.\n5. [05-migration.md](./05-migration.md) — legacy class → primitive mapping,\n   what gets deleted, and the incremental migration order.\n\n## Design lineage\n\nThe API descends from SolidJS (ownership tree, `createResource`'s async\nstates, `untrack`, lazy equality-gated memos, `from()` interop), Jotai/Recoil\n(`atomFamily` → `family`), and TanStack Query (`setQueryData` vs\n`invalidateQueries` → `settle` vs pokes). The one deliberate departure from\nSolid: dependency tracking is automatic only where reads are interceptable\n(`derived` over in-process reactives); at the external-source boundary\n(`query` over DB/fs/git), dependencies are *declared* as pokes so that \"poke\ncoverage = query inputs\" stays a greppable, auditable property.\n","isInternal":false,"tokens":1532,"sizeBytes":6091}],"systemPromptSnippet":"<yakaai_skills repo=\"generalaction/emdash\">\n<!-- File: AGENTS.md (Tokens: ~5638 | Category: root-instruction) -->\n# Project Overview\n\nEmdash is a local-first, cross-platform Electron app for running multiple AI coding\nagents in parallel. Each task is isolated in its own Git worktree and can run locally\nor over SSH, while the desktop app coordinates provider CLIs, ACP chat sessions,\nterminal sessions, issue and PR integrations, diff review, and release packaging.\n\n## Repository Structure\n\nThis is a pnpm workspace monorepo. The Electron app lives in\n`apps/emdash-desktop/` as `@emdash/emdash-desktop`; shared packages live under\n`packages/`. Unless a path is prefixed with `packages/` or another app, app paths\nsuch as `src/...`, `drizzle/`, `scripts/`, and `build/` are relative to\n`apps/emdash-desktop/`.\n\nRepo root:\n\n- `.claude/` - Local Claude agent settings for this checkout.\n- `.github/` - GitHub issue templates, reusable actions, CI, and release workflows.\n- `agents/` - Agent-facing architecture, workflow, convention, integration, and risk docs.\n- `apps/emdash-desktop/` - The Electron desktop app.\n- `apps/workspace-server/` - Remote workspace server and its Docker-based dev stack.\n- `packages/chat-ui/` - Shared transcript and ACP chat renderer with Storybook coverage.\n- `packages/core/` - Transport-agnostic runtime primitives, including ACP session logic.\n- `packages/plugins/` - Agent provider plugin definitions, hooks, and ACP adapters.\n- `packages/shared/` - Shared primitives such as result types, logging, and markdown helpers.\n- `packages/theme/` - Theme token codegen that emits the generated theme CSS.\n- `packages/ui/` - Shared React UI components, theme tokens, recipes, and primitives.\n- `packages/wire/` - Typed wire protocol contracts shared by app and workspace server.\n- `pnpm-workspace.yaml` - Workspace package globs for `apps/*` and `packages/**`.\n- Root config files - `package.json`, `nx.json`, `.nvmrc`, `.oxfmtrc.json`,\n  `.oxlintrc.json`, and lockfile/configuration owned at the workspace root.\n\nInside `apps/emdash-desktop/`:\n\n- `build/` - Electron packaging assets; avoid edits unless working on packaging/signing.\n- `drizzle/` - Generated Drizzle SQL migrations and metadata.\n- `scripts/` - Release, verification, and build support scripts.\n- `src/main/` - Electron main process: bootstrap, Wire gateway, DB, and host services.\n- `src/entry/` - Electron entry points: `main.ts` (main process) and `preload.ts`, the typed\n  preload bridge exposed to the renderer.\n- `src/renderer/` - React composition shell, shared browser infrastructure, and tests.\n- `src/core/` - Vertical slices with portable APIs, Node implementations, browser UI, and manifests.\n- `src/types/` - Ambient and cross-cutting TypeScript declarations.\n- `tooling/` - App-level development and test infrastructure not bundled into production.\n\n## Build & Development Commands\n\nThe only prerequisite is any `pnpm` on PATH: the root `package.json` pins\n`packageManager: pnpm@10.28.2` and `devEngines.runtime` node `24.14.0` with\n`onFail: \"download\"`, so pnpm provisions the pinned toolchain itself (no nvm or\ncorepack). The committed `mise.toml` is an optional convenience for mise users.\nRoot scripts are powered by Nx and run package targets in dependency order with\nlocal caching where configured.\n\nGolden-path commands (see [Developer flows](agents/workflows/flows.md) for the\nfull index — one blessed command per flow, database flows, Storybook,\npackaging, remote development, and escape hatches):\n\n```bash\npnpm install        # complete setup from the repo root — nothing else needed\npnpm run doctor     # report-only environment health check\npnpm run dev        # full workspace dev (root); app-only from apps/emdash-desktop/\npnpm run build      # build all workspace projects\npnpm run check      # full merge gate: format, lint, typecheck, test\n```\n\nThe four gate commands also run individually from the repo root:\n\n```bash\npnpm run format\npnpm run lint\npnpm run typecheck\npnpm run test\n```\n\nUse an isolated development database for schema or migration work:\n\n```bash\nEMDASH_DB_FILE=/tmp/emdash-scratch.db pnpm run dev\n```\n\nRun focused database validation from `apps/emdash-desktop/`:\n\n```bash\npnpm run db:fixtures\npnpm run test:migrations\n```\n\nDeploy releases only when explicitly asked to do release work:\n\n```bash\ngh workflow run release-prod.yml --ref main -f arch=both\ngh workflow run release-canary.yml --ref main -f arch=both\ngh workflow run release-workspace-server.yml --ref main -f channel=stable\n```\n\nProduction releases publish artifacts to GitHub Releases and Cloudflare R2. Canary\nreleases currently publish to R2 only. See\n[workspace-server packaging](apps/workspace-server/docs/packaging.md) for its independent release\nand channel rules.\n\n## Code Style & Conventions\n\n- Use `pnpm`; do not introduce npm or yarn lockfile churn.\n- Format with `oxfmt`; config is `.oxfmtrc.json`.\n- Keep formatted lines near the configured `printWidth` of 100 characters.\n- Use 2 spaces, semicolons, single quotes in TS, double quotes in JSX, LF endings,\n  trailing commas where valid in ES5, and sorted imports.\n- Lint with `oxlint`; config is `.oxlintrc.json` with correctness, TypeScript, React\n  hooks, and local repo rules enabled.\n- TypeScript strict mode is enabled; app targets share `apps/emdash-desktop/tsconfig.json`.\n- Avoid `any`; if a registry or boundary needs it, keep the escape local and documented.\n- Use top-level `import` statements; do not use `require()`.\n- Never re-export as a shortcut; import from the original source.\n- Components use `PascalCase`; hooks use `useX` camelCase or an existing local pattern.\n- Tests use `*.test.ts` or `*.test.tsx`.\n- Slice Wire controllers live in `src/core/features/<feature>/node/` and are aggregated by\n  `src/core/manifests/node/controllers.ts`; they delegate to imported operation or service\n  functions.\n- Renderer-main calls go through Wire via the seeded connection seam in\n  `src/core/primitives/wire/browser/connection.ts`: the renderer bootstrap seeds it once at\n  startup, and each slice exposes a typed domain client from its `api/` (for example\n  `getMcpClient()`) built on `domainClient`.\n- Feature UI lives under `src/core/features/<feature>/browser/`; the editor slice owns Monaco and\n  file rendering under `src/core/features/editor/browser/`; portable browser primitives (modals,\n  views, navigation, commands, scoped stores) live under `src/core/primitives/`; the thin\n  renderer host shell lives under `src/renderer/lib/`.\n- New feature modals and views must be exposed through the owning slice's `contributions/browser.ts`\n  and aggregated by `src/core/manifests/browser/browser-contributions.ts`.\n- New task tabs contribute providers through `src/core/features/<feature>/contributions/tabs.ts`,\n  aggregated by `src/core/manifests/browser/task-tab-contributions.ts`; the workbench registry\n  lives at `src/core/features/workbench/api/browser/task-tab-registry.ts`.\n- New commands are defined with `defineCommand` (`src/core/primitives/commands/`) in the owning\n  slice's `contributions/commands.ts`, aggregated by\n  `src/core/manifests/shared/command-catalog.ts`; view scopes\n  (`src/core/primitives/view-scopes/`) bind availability and implementation.\n- Commit messages should follow Conventional Commits:\n\n```text\n<type>(<scope>): <short imperative summary>\n\nExamples:\nfix(opencode): change initialPromptFlag from -p to --prompt for TUI\nfeat(docs): add changelog tab with GitHub releases integration\n```\n\n## Architecture Notes\n\n```mermaid\nflowchart LR\n  User[User] --> Renderer[React renderer]\n  Renderer --> Clients[Typed Wire domain clients]\n  Clients --> Seam[Seeded wire connection seam]\n  Seam --> Preload[Preload wire port]\n  Preload --> Main[Electron main process]\n  Main --> Gateway[Desktop Wire gateway]\n  Gateway --> Controllers[Slice Wire controllers]\n  Controllers --> Services[Domain services]\n  Services --> DB[(SQLite via Drizzle)]\n  Services --> Runtime[Runtime services]\n  Runtime --> PTY[PTY sessions]\n  Runtime --> ACP[ACP sessions]\n  Runtime --> SSH[SSH and SFTP]\n  Services --> VCS[Git, GitHub, GitLab, PRs]\n  Services --> Issues[Issue integrations]\n  Services --> MCP[MCP and skills]\n  ACP --> CoreAcp[@emdash/core ACP runtime]\n  ACP --> Plugins[@emdash/plugins providers]\n  Renderer --> ChatUI[@emdash/chat-ui]\n  Main --> Events[Live models and typed events]\n  Events --> Renderer\n```\n\nThe app boots from `src/entry/main.ts`, which runs the phased bootstrap in\n`src/main/bootstrap/` (environment, database, window creation, recovery). Renderer-main\ntraffic is served by the desktop Wire gateway (`src/main/gateway/desktop-wire.ts`) from the\ncontract assembled in `src/core/manifests/shared/desktop-wire-contract.ts` and the controllers\naggregated in `src/core/manifests/node/controllers.ts`. The preload bridge\n(`src/entry/preload.ts`) exposes only `requestWirePort` and `getPathForFile`. The renderer is\na React app that calls typed Wire domain clients, subscribes to live models and typed events,\nand coordinates views, tabs, modals, commands, project state, terminal state, and task\nworkflows.\n\nTask execution has two runtime paths. Legacy/TUI conversations run through PTY\nservices under `packages/core/src/services/pty/` and the terminal/TUI runtimes under\n`packages/core/src/runtimes/terminals/` and `packages/core/src/runtimes/tui-agents/`.\nStructured chat conversations use ACP: provider plugins in `packages/plugins/` expose\nACP behavior, `packages/core/src/runtimes/acp/` owns protocol/session state, terminal\nmanagement, and process hosting, the desktop launches that runtime as a Wire component worker\nfrom `src/main/gateway/entries/acp.ts` (`src/main/core/acp/` bridges agent status), and\n`src/core/features/conversations/browser/acp/` maps updates into `@emdash/chat-ui`.\n\nMain-process adapter domains live under `src/main/core/`: ACP, agent status, app,\ndependencies, file search, files, Git, preview servers, provider accounts, runtime,\nshared, terminal shell, and utils. Portable domain logic lives in vertical slices\nunder `src/core/` and in `packages/core/` (for example PTY services under\n`packages/core/src/services/pty/` and resource monitoring under\n`packages/core/src/runtimes/resource-usage/`). Expected failures should use the\n`Result<T, E>` pattern from `@emdash/shared` or the app-local result helpers.\n\n## Testing Strategy\n\nLocal merge gate:\n\n```bash\npnpm run format\npnpm run lint\npnpm run typecheck\npnpm run test\n```\n\n- Root `pnpm run test` uses Nx to run every workspace package test target.\n- App tests run with Vitest projects in `apps/emdash-desktop/vitest.config.ts`.\n- App `node` tests cover `src/**/*.test.ts` except DB, migration, and browser tests.\n- App `main-db` tests cover main-process integration tests that need real SQLite.\n- App `fixtures` tests generate DB fixtures via `pnpm run db:fixtures`.\n- App `migrations` tests validate Drizzle migrations via `pnpm run test:migrations`.\n- App `scripts` tests cover release and support scripts under `scripts/**/*.test.ts`.\n- App `browser` tests use Playwright-backed `@vitest/browser` for renderer behavior.\n- `packages/core` has ACP, dependency, plugin helper, Git, FS, and runtime unit tests.\n- `packages/chat-ui` has node, browser, perf, and benchmark test targets.\n- `packages/ui`, `packages/shared`, and `packages/plugins` run their package-local tests.\n- Integration-style tests create temporary repos and worktrees in `os.tmpdir()`.\n- CI runs `.github/workflows/code-consistency-check.yml` with `nx affected` for\n  `format:check`, `typecheck`, `lint`, and `test` on touched projects and\n  dependents; the Playwright-backed `browser` Vitest projects are skipped there.\n- Tests are still expected locally before merge even where CI coverage is narrower.\n\n## Security & Compliance\n\n- The project is licensed under Apache-2.0; see `LICENSE.md`.\n- Do not commit secrets, tokens, private keys, app databases, logs, build artifacts,\n  generated dependency folders, or release artifacts.\n- Application secrets are stored through encrypted app secret services and Electron\n  safe storage; SSH credentials are managed through SSH services and OS-backed storage.\n- Release secrets live in GitHub Actions secrets, including PostHog, Cloudflare R2,\n  Apple signing/notarization, Azure Trusted Signing, and Cachix credentials.\n- Telemetry must remain optional; users can disable it with `TELEMETRY_ENABLED=false`\n  or in app settings.\n- File logging must preserve redaction of common secret patterns.\n- PTY environment construction must go through `packages/core/src/services/pty/api/terminal-env.ts`.\n- Treat ACP process spawning, SSH command construction, shell escaping, PTY spawning,\n  and worktree paths as security-sensitive.\n- Do not bypass path-safety, shell escaping, or validation helpers.\n- Use `pnpm-lock.yaml` for dependency integrity and review dependency changes.\n- This checkout does not define repo-local Dependabot config, CODEOWNERS, or SECURITY.md;\n  do not assume extra repository-owned security automation beyond the workflows present\n  in `.github/workflows/`.\n- Dependency changes must keep `pnpm-lock.yaml` in sync, preserve `packageManager` and\n  `pnpm.onlyBuiltDependencies`, and avoid introducing new install scripts or native\n  builds without explicit review.\n- CI installs with `pnpm install --frozen-lockfile --ignore-scripts` in\n  `.github/workflows/code-consistency-check.yml`; changes that rely on install-time\n  side effects need clear justification.\n- Prefer existing dependencies and workspace packages over adding new third-party\n  packages. When adding a dependency, document why the existing stack is insufficient\n  and check license/security posture before committing the lockfile change.\n\n## Agent Guardrails\n\n- Start with this file for repo-wide context and required commands.\n- Load only the relevant `agents/` topic page for the area you are changing.\n- Prefer updating the smallest applicable `agents/` page over expanding this file.\n- If nested `AGENTS.md` files are added later, the closest file to the edited path wins.\n- Explicit user or maintainer instructions override this file.\n- Do not hand-edit numbered Drizzle migrations or `drizzle/meta/`.\n- Use `pnpm run db:generate` for new migrations, then update fixtures and migration tests.\n- Avoid editing `dist/`, `release/`, `out/`, `build/`, and generated package artifacts\n  unless the task is explicitly about packaging, signing, or release behavior.\n- Do not dispatch release workflows, publish packages, upload artifacts, or trigger\n  external deployments unless the user explicitly asks for release work.\n- Treat `src/main/core/acp/`, `packages/core/src/services/pty/`,\n  `src/core/services/ssh/`, `src/main/db/`, updater code, and provider process\n  spawning as high risk.\n- Read the matching `agents/risky-areas/` page before touching database, PTY, SSH, or\n  updater code.\n- Do not weaken shell quoting, spawn behavior, env allowlists, path validation, or\n  secret redaction casually.\n- Prefer existing service, provider, plugin, Wire, modal, view, tab, and store patterns\n  over new abstractions.\n- New Wire procedures belong in the owning slice's `api/` contract with a controller in\n  `node/`, registered through `src/core/manifests/shared/desktop-wire-contract.ts` and\n  `src/core/manifests/node/controllers.ts`.\n- Keep renderer-main calls on typed Wire contracts, live models, and typed events. The preload\n  bridge should stay small; add direct `window.electronAPI` surface only when an\n  Electron/browser primitive cannot fit the Wire path.\n- Access task and Project MobX stores through selectors and task view hooks:\n  `getTaskStore`, `asProvisioned`, `taskViewKind`, `getTaskManagerStore`,\n  `getProjectStore`, `asAvailableProject`, `getProjectHostAccess`, `useTaskViewKind`, `useWorkspace`,\n  `useWorkspaceId`, `useDevServers`, `useWorkspaceViewModel`, `useConversations`,\n  and `useTerminals`.\n- Never use `asProvisioned(...)!` or `asAvailableProject(...)!`; use explicit null checks.\n- State guards must check `kind !== 'ready'` rather than enumerating non-ready states.\n- Access task managers through `getTaskManagerStore(projectId)`, not `project.taskManager`.\n- Access available Project contexts through `asAvailableProject(getProjectStore(id))`, not inline\n  guards. Read Host-dependent action state through that context's `host` access interface.\n- Task selectors live in `src/core/features/tasks/api/browser/task-state/task-selectors.ts`.\n- Project selectors live in `src/core/features/projects/api/browser/stores/project-selectors.ts`.\n- For provider changes, update plugin metadata, shared provider metadata, ACP support\n  flags, PTY env passthrough if needed, hook integrations, renderer assumptions, and\n  tests for non-standard behavior.\n- For ACP changes, preserve protocol state-machine behavior in `packages/core/src/runtimes/acp/`,\n  keep provider-specific transforms in `packages/plugins/`, and adapt UI payloads at\n  app or chat-UI edges.\n- For MCP changes, keep canonical data in shared types and adapt provider formats at edges.\n- Follow `.github/PULL_REQUEST_TEMPLATE.md`: keep PRs small and focused, self-review\n  before handoff, list checks run, attach UI evidence when applicable, and update docs\n  and tests when behavior changes.\n- Call out high-risk changes explicitly in the PR description or handoff notes,\n  especially database, ACP, PTY, SSH, updater, provider spawning, dependency, and\n  release-related changes.\n- Do not self-approve, merge, assign reviewers, edit branch protection, or change\n  workflow permissions unless the user explicitly asks.\n- Avoid scripted loops against GitHub, Linear, Jira, GitLab, provider CLIs, or release\n  workflows. Use focused queries and respect existing workflow retries; ask before\n  adding polling, bulk API calls, or scheduled automation.\n- Keep automation scoped to the task. Do not run the full local merge gate repeatedly\n  when a focused check is enough during iteration; run broader checks before handoff\n  when the change scope justifies it.\n\n## Extensibility Hooks\n\n- Agent provider plugins live in `packages/plugins/src/agents/impl/` and are registered\n  in `packages/plugins/src/agents/registry.ts`.\n- Provider plugin hosting services live in `packages/core/src/services/agent-plugins/`.\n- Agent provider metadata and capabilities live in `packages/plugins/src/agents/registry.ts`\n  and `packages/plugins/src/agents/impl/`; renderer-facing DTOs are built by\n  `src/core/features/agents/node/agent-payload-builder.ts`.\n- ACP support is exposed through plugin ACP capabilities and portable Core ACP APIs.\n- Provider detection lives in `src/main/core/dependencies/`.\n- Provider PTY behavior and env construction live under `packages/core/src/services/pty/`.\n- Provider event hooks and plugins are installed and hosted by\n `packages/core/src/runtimes/tui-agents/`; desktop projection lives under\n `src/main/core/agent-status/`.\n- ACP process hosting lives in `packages/core/src/runtimes/acp/`; the desktop launches the\n  runtime as a Wire component worker from `src/main/gateway/entries/acp.ts`.\n- Feature modal and view definitions are contributed by slices and aggregated in\n  `src/core/manifests/browser/browser-contributions.ts`; renderer app registries compose host entries.\n- Task tab providers are aggregated through\n  `src/core/manifests/browser/task-tab-contributions.ts`.\n- MCP server config handling lives in `src/core/features/mcp/node/` (wire controller and\n  registration utils), canonical types in `src/core/primitives/mcp/api/`, and UI in\n  `src/core/features/mcp/browser/`.\n- Skills types and validation live in Core primitives; skills UI and service code live in\n  `src/core/features/skills/browser/` and `src/core/features/skills/node/`.\n- Team-owned runtime settings live in each workspace's `.emdash.json`:\n  `preservePatterns`, `scripts.prepare`, `scripts.setup`, `scripts.run`,\n  `scripts.teardown`, and `shellSetup` (a per-workspace override of the host default).\n- Project overrides such as `worktreeRoot`, `defaultBranch`, `baseRemote`, `pushRemote`,\n  `githubAccount`, `agentGitCredentials`, and `tmux` are DB-backed, not `.emdash.json`. Effective\n  tmux resolves through project override > host default > app default; it is not seeded from a\n  default at project creation.\n- The workspace registry owns lifecycle and file-handling config. Host-local personal config\n  (`preservePatterns`, scripts, and auto-run toggles) is versioned JSON on the repository or\n  directory project-root record; worktrees resolve against their project root's personal config.\n- Read registry-owned config through the keyed `projectConfig` live model or `getProjectConfig`.\n  It exposes raw personal/team layers, team-file sources, and one `{ value, from }` result per\n  field. Applicable precedence is personal > that workspace's `.emdash.json` > host default >\n  built-in; arrays replace rather than merge.\n- Per-host defaults (`shellSetup`, `worktreeRoot`, `tmux`) live in the host-settings\n  runtime (`packages/core/src/runtimes/host-settings/`), stored as JSON in the host's\n  emdash data directory and editable from the machines/system settings UI.\n- Desktop project settings compose self-contained domain snapshots: the registry owns\n  lifecycle/file handling, while desktop DB and host-settings domains own Git identity and\n  placement. Forms edit raw layers with explicit per-domain patches; there is no merged settings\n  bag to persist.\n- The scripts runtime is a strict executor: callers must provide resolved `command` and\n  `shellSetup`; it never reads config files or host defaults. The workspace registry resolves and\n  sequences lifecycle commands, then observes runs into durable lifecycle steps.\n- Desktop settings migrations are centralized under\n  `src/core/features/projects/node/settings/migrations/` and run in order when an attachment is\n  established. Legacy schemas and readers stay migration-only; destination markers make imports\n  idempotent and retryable.\n- Optional environment variables include `TELEMETRY_ENABLED`, `EMDASH_DB_FILE`,\n  `EMDASH_DISABLE_NATIVE_DB`, `EMDASH_DISABLE_PTY`, `EMDASH_AUTH_SERVER_URL` (development\n  builds only), `CODEX_SANDBOX_MODE`, and `CODEX_APPROVAL_POLICY`.\n- Build-time telemetry configuration may use `VITE_POSTHOG_KEY` and `VITE_POSTHOG_HOST`.\n- Runtime feature flags are read through telemetry-backed feature flag helpers.\n- App-internal path aliases are defined in `tsconfig.json` (`@/*`, `@core/*`, `@renderer/*`,\n  `@main/*`, `@root/*`, and `@tooling/*`) and mirrored where needed in\n  `electron.vite.config.ts`; workspace packages resolve through their `exports` maps with a\n  `development` condition, not aliases.\n- Versioned JSON column schemas use `defineVersionedSchema()` from\n  `@emdash/core/primitives/versioned-schema/api`\n (`packages/core/src/primitives/versioned-schema/api/versioned-schema.ts`) and Drizzle\n `versionedJsonColumn()` from `src/core/services/app-db/node/versioned-column.ts`.\n\n## Further Reading\n\n- [Agent docs map](agents/README.md)\n- [Quickstart](agents/quickstart.md)\n- [Architecture overview](agents/architecture/overview.md)\n- [Settings ownership and precedence](agents/architecture/settings.md)\n- [Main process architecture](agents/architecture/main-process.md)\n- [Renderer architecture](agents/architecture/renderer.md)\n- [Shared modules](agents/architecture/shared.md)\n- [Developer flows](agents/workflows/flows.md)\n- [Nx task orchestration and caching](agents/workflows/nx.md)\n- [Testing workflow](agents/workflows/testing.md)\n- [Worktrees workflow](agents/workflows/worktrees.md)\n- [Remote development workflow](agents/workflows/remote-development.md)\n- [Workspace server architecture](agents/architecture/workspace-server.md)\n- [Provider integration](agents/integrations/providers.md)\n- [MCP integration](agents/integrations/mcp.md)\n- [IPC conventions](agents/conventions/ipc.md)\n- [Main-process patterns](agents/conventions/main-patterns.md)\n- [Renderer patterns](agents/conventions/renderer-patterns.md)\n- [TypeScript and React conventions](agents/conventions/typescript.md)\n- [Config file rules](agents/conventions/config-files.md)\n- [UI kit conventions](agents/conventions/ui-kit.md)\n- [UI styling conventions](agents/conventions/ui-styling.md)\n- [Versioned schema conventions](agents/conventions/versioned-schemas.md)\n- [Database risk notes](agents/risky-areas/database.md)\n- [PTY risk notes](agents/risky-areas/pty.md)\n- [SSH risk notes](agents/risky-areas/ssh.md)\n- [Updater risk notes](agents/risky-areas/updater.md)\n- [Contributing guide](CONTRIBUTING.md)\n- [Project README](README.md)\n\n<!-- File: CLAUDE.md (Tokens: ~5 | Category: claude-rule) -->\n@AGENTS.md\n\n<!-- File: packages/core/docs/path/boundaries.md (Tokens: ~779 | Category: anthropic-skill) -->\n# Boundaries\n\nThe path module owns pure identity and lexical path math. It does not own host\nlifecycle, filesystem I/O, authorization, or persistence migrations.\n\n## Conversion Points\n\n```mermaid\nflowchart LR\n  NativeInput[Native string] --> Parser[Path parser]\n  Parser --> Ref[HostFileRef]\n  Ref --> Controller[Domain controller]\n  Controller -->|select by HostRef| Wire[Host-bound Wire connection]\n  Wire --> Runtime[Runtime path key]\n  Runtime --> NativeOutput[Native string]\n  NativeOutput --> Fs[Filesystem or Git]\n```\n\nConvert at these boundaries:\n\n- User input, picker output, SSH responses, and Git roots become structured refs\n  at ingress.\n- Renderer and persistence payloads use `ResourceUri`, `HostFileRef`, or\n  `ScopedPath`, not raw native strings.\n- Domain controllers use `HostRef` to select or relay to a host-bound Wire\n  connection. They do not inject host routing identity into the runtime.\n- Host-bound Wire payloads use `HostAbsolutePath` roots and\n  `PortableRelativePath` coordinates.\n- Host runtimes convert structured paths back to native strings immediately before\n  filesystem, Git, watcher, or process-spawn calls.\n- Watcher and Git events should become `ScopedPath` values before crossing to\n  renderer-facing models.\n\n## Contract Validation\n\nContracts should import schemas from `@emdash/core/path` instead of introducing\nnew `z.string()` path fields once they migrate to structured resources.\n\nRecommended boundary shapes:\n\n- `hostFileRefSchema` for one-off global file resources.\n- `scopedPathSchema` for a single file under a known root.\n- `{ root: hostAbsolutePathSchema, path: portableRelativePathSchema }` for\n  host-bound runtime APIs, tree keys, watcher events, and Git paths.\n- `{ root: hostFileRefSchema, path: portableRelativePathSchema }` while a payload\n  still needs host routing.\n- `resourceUriSchema` for persisted string identity.\n- `absolutePathInputSchema(profile)` for native/user/host input only.\n\nThe schemas delegate to the same parsers as internal code, so contract\nvalidation and local validation enforce the same invariants.\n\n## Lexical Versus Physical Containment\n\n`containsAbsolute()` and `relativizeHostFileRef()` are lexical. They do not\nfollow symlinks.\n\nUse lexical containment for identity, tree projection, deduplication, batching,\nand root-relative path conversion.\n\nUse realpath-based containment for security-sensitive writes, deletes, and\nfilesystem mutation policies. That logic belongs in runtime or app services with\naccess to a filesystem implementation.\n\n## Not Owned Here\n\nThis module intentionally does not implement:\n\n- host connection ownership or mapping from `HostRef` to a live runtime;\n- SSH connection management;\n- workspace mount persistence;\n- `realpath()` or canonical inode checks;\n- renderer tab migrations;\n- Monaco model migration;\n- Wire contract migrations;\n- filesystem authorization.\n\n## Future Adoption Map\n\nExisting consumers can migrate incrementally:\n\n- Files contracts use `HostAbsolutePath` roots and `PortableRelativePath`\n  coordinates; runtime policy adds realpath-based mutation safety.\n- Desktop `RuntimePath` can become a thin native-format adapter around\n  `PathSemantics`.\n- SSH path helpers can use POSIX parsing explicitly instead of app-local string\n  utilities.\n- Git models use repository/worktree `HostAbsolutePath` roots and keep file paths\n  as `PortableRelativePath`.\n- fs-watch can emit root-scoped portable paths, then batch using `ResourceKey`.\n- workspace-server schemas can validate structured refs instead of documenting\n  string conventions only.\n- Renderer tabs, tree nodes, Monaco models, comments, and view-state snapshots\n  can store `ResourceUri` while deriving display paths from workspace mounts.\n\n<!-- File: packages/core/docs/path/normalization.md (Tokens: ~474 | Category: anthropic-skill) -->\n# Normalization\n\nPath normalization in Emdash is explicit. Callers choose the dialect they are\nparsing instead of inheriting the operating system of the current process.\n\n## Native Absolute Paths\n\n`parseAbsolute()` accepts a `profile` option.\n\n```ts\nparseAbsolute('/home/david/repo', { profile: { style: 'posix' } });\nparseAbsolute('C:\\\\Users\\\\David\\\\repo', { profile: { style: 'win32' } });\nparseAbsolute('\\\\\\\\server\\\\share\\\\repo', { profile: { style: 'win32' } });\n```\n\nPOSIX parsing treats backslash as a valid filename character. Windows parsing\ntreats both `\\\\` and `/` as separators.\n\nThe parser:\n\n- rejects null bytes;\n- resolves `.` and `..` lexically;\n- rejects paths that escape their root;\n- preserves Unicode spelling by default and normalizes only when requested;\n- preserves path spelling and casing for display;\n- uppercases Windows drive letters in the structured root.\n\nThe parser does not:\n\n- call `realpath()`;\n- check whether the path exists;\n- authorize access;\n- turn a POSIX backslash into a separator.\n\n## Portable Relative Paths\n\n`PortableRelativePath` is used for scoped coordinates such as file tree keys,\nGit paths, watcher events, and root-relative Wire payloads.\n\nRules:\n\n- `/` is the separator.\n- The empty string is the root.\n- Leading `/`, Windows drive roots, and UNC roots are rejected.\n- `.` and `..` are resolved lexically; escaping the root is rejected.\n- Backslash is a literal character, not a separator.\n- Null bytes are rejected.\n\n```ts\nparsePortableRelativePath('src/./components/../index.ts');\n// => 'src/index.ts'\n\nparsePortableRelativePath('../outside');\n// => invalid-path\n```\n\n## Comparison\n\nStored paths preserve spelling. Equality and deduplication use comparison keys\ncreated from an explicit `PathProfile`.\n\n```ts\nconst semantics = createPathSemantics({\n  style: 'win32',\n  caseSensitivity: 'insensitive',\n  unicodeNormalization: 'nfc',\n});\n```\n\nDo not lowercase or otherwise rewrite stored paths to make comparison easier.\nUse `ResourceKey` or `PathSemantics.comparisonKey()` for maps and sets.\n\n<!-- File: packages/core/docs/path/recipes.md (Tokens: ~736 | Category: anthropic-skill) -->\n# Recipes\n\n## Parse a Native POSIX Path\n\n```ts\nconst parsed = parseAbsolute('/home/david/repo/src/index.ts', {\n  profile: { style: 'posix' },\n});\n```\n\n## Parse a Native Windows Path\n\n```ts\nconst parsed = parseAbsolute('C:\\\\Users\\\\David\\\\repo\\\\src\\\\index.ts', {\n  profile: { style: 'win32' },\n});\n```\n\n## Build a Host File Ref\n\n```ts\nconst host = hostRef('remote', 'connection-1');\nconst path = parseAbsolute('/repo/src/index.ts', { profile: { style: 'posix' } });\n\nif (path.success) {\n  const ref = hostFileRef(host, path.data);\n}\n```\n\n## Use a Root-Scoped Path\n\n```ts\nconst root = hostFileRef(LOCAL_HOST_REF, rootPath);\nconst relative = parsePortableRelativePath('src/index.ts');\n\nif (relative.success) {\n  const scoped = scopedPath(root, relative.data);\n  const resolved = resolveScopedPath(scoped);\n}\n```\n\nUse this pattern for watcher events, Git paths, file tree nodes, and bulk file\noperations.\n\n## Store a Resource URI\n\n```ts\nconst uri = encodeResourceUri(ref);\nconst decoded = decodeResourceUri(uri);\n```\n\nStore the URI when a durable string identity is needed. Keep `ResourceKey` for\nin-memory maps only.\n\n## Deduplicate with a Resource Key\n\n```ts\nconst key = resourceKeyFromFileRef(ref, {\n  profile: {\n    style: 'win32',\n    caseSensitivity: 'insensitive',\n    unicodeNormalization: 'nfc',\n  },\n});\n\nmodelsByResource.set(key, model);\n```\n\n## Convert an Event Under a Watched Root\n\n```ts\nconst changed = hostFileRef(root.host, changedAbsolutePath);\nconst relative = relativizeHostFileRef(root, changed);\n\nif (relative.success) {\n  const scoped = scopedPath(root, relative.data);\n}\n```\n\nBatch and coalesce watcher events with resource keys derived from the resolved\nfile refs, not by string-prefix comparisons.\n\n## Validate a Contract Model\n\nUse structured schemas for contracts instead of ad hoc `z.string()` path fields.\n\n```ts\nconst readTextInputSchema = z.object({\n  file: scopedPathSchema,\n  options: readFileOptionsSchema.optional(),\n});\n```\n\nFor root-scoped bulk APIs, keeping root and relative path separate is often more\ncompact:\n\n```ts\nconst treePathKeySchema = z.object({\n  root: hostAbsolutePathSchema,\n  path: portableRelativePathSchema,\n  sessionId: z.string(),\n});\n```\n\nThis compact form is appropriate after a domain controller has already selected\nthe host-bound runtime connection. Use `hostFileRefSchema` instead when the same\npayload must still be routed to a host.\n\nFor durable UI state, use a resource URI:\n\n```ts\nconst fileTabStateSchema = z.object({\n  resource: resourceUriSchema,\n});\n```\n\nOnly use native absolute input schemas at ingress boundaries:\n\n```ts\nconst nativePickerInputSchema = z.object({\n  path: absolutePathInputSchema({ profile: { style: 'posix' } }),\n});\n```\n\n## Handle Errors\n\nAll parsing helpers return `Result<T, PathError>`.\n\n```ts\nconst parsed = parsePortableRelativePath(input);\nif (!parsed.success) {\n  switch (parsed.error.type) {\n    case 'invalid-path':\n      return parsed.error.message;\n    case 'outside-root':\n      return 'Path is outside the selected root';\n    default:\n      return 'Invalid resource';\n  }\n}\n```\n\n<!-- File: packages/core/docs/path/resources.md (Tokens: ~694 | Category: anthropic-skill) -->\n# Resources\n\n`HostFileRef` is the canonical application-level file identity:\n\n```ts\ntype HostFileRef = {\n  host: HostRef;\n  path: HostAbsolutePath;\n};\n```\n\nIt identifies a logical path on a host. It is not an inode identity. Symlinks,\nhard links, and renames are handled by runtime-specific filesystem behavior.\n\n## Host References\n\n`HostRef` is defined by `@emdash/core/primitives/host/api`:\n\n```ts\ntype HostRef = {\n  type: 'local' | 'remote';\n  id: string;\n};\n```\n\nIt is used by domain controllers to select a host-bound runtime connection. A\nruntime executes on the addressed host and therefore does not need `HostRef` in\nits own Git or Files keys.\n\n## Absolute Roots\n\nStructured roots avoid ambiguity across hosts and drives:\n\n```ts\n{ kind: 'posix' }\n{ kind: 'drive', driveLetter: 'C' }\n{ kind: 'unc', server: 'server', share: 'share' }\n```\n\nThe path segments are stored separately from the root, so `/`, `C:/`, and\n`//server/share` cannot be confused.\n\n## Scoped Paths\n\nUse `ScopedPath` when many operations share a root:\n\n```ts\ntype ScopedPath = {\n  root: HostFileRef;\n  relative: PortableRelativePath;\n};\n```\n\nThis is the preferred globally addressable shape for watcher events, tree\nentries, and batch operations. Host-local runtime contracts use the equivalent\ncompact shape: a `HostAbsolutePath` root plus `PortableRelativePath` coordinates.\n\n## Resource URIs\n\n`ResourceUri` is the stable string encoding:\n\n```text\nemdash-file://v2/local/local/posix/home/david/repo/src/index.ts\nemdash-file://v2/remote/connection-1/drive/c/Users/David/repo/src/index.ts\nemdash-file://v2/remote/connection-2/unc/server/share/repo/src/index.ts\n```\n\nUse it for serialization, persistence, Monaco model identity, and durable\nmessages. Percent encoding is applied per path segment.\n\n## Contract Schemas\n\nPath schemas live next to the path primitives so Wire/RPC contracts and\npersisted models do not duplicate validation rules.\n\nUse:\n\n- `hostFileRefSchema` for a globally addressable file resource.\n- `scopedPathSchema` for a file under a known root.\n- `portableRelativePathSchema` for Git paths, file tree entry keys, and watcher\n  paths that are already scoped by another field.\n- `resourceUriSchema` for persisted or Monaco-style string identity.\n- `resourceRefFromUriSchema` when a boundary accepts a URI string but internal\n  code wants a decoded `HostFileRef`.\n- `absolutePathInputSchema(profile)` only at native/user/host ingress\n  boundaries.\n\nSchemas delegate to the canonical parsers. For example,\n`portableRelativePathSchema` transforms `src/./index.ts` into `src/index.ts`\ninstead of merely branding the original string.\n\n## Resource Keys\n\n`ResourceKey` is an opaque comparison key for maps and deduplication. It may be\ncase-insensitive or Unicode-normalized depending on the selected profile.\n\nDo not persist `ResourceKey` as the only representation of a file. It is lossy by\ndesign and may not preserve display spelling.\n\n<!-- File: packages/wire/docs/api/clients.md (Tokens: ~1186 | Category: anthropic-skill) -->\n# Typed Clients\n\n`client(contract, connection)` creates a typed `ContractClient`. It has the\nsame nested shape as the contract, but it does not materialize live state.\n\nThink of it as a type-safe wrapper around `Connection`: it knows contract paths,\ninput/output types, live topic ids, group keys, and mutation envelopes, but it does\nnot keep snapshots, apply patches, or settle mutations against local state.\n\n```ts\nconst contractClient = client(api, connect(transport));\n\nawait contractClient.ping({ message: 'hello' });\n\nconst state = contractClient.conversation.state(key, 'state');\nconst snapshot = await state.snapshot();\nconst detach = await state.attach((update) => {\n  console.log(update.delta);\n});\n```\n\n## Endpoint Shapes\n\nEach contract endpoint becomes a protocol client handle:\n\n- Procedures become typed functions: `contractClient.ping(input, { signal? })`.\n- Live logs become `LiveLogClientHandle`s: `contractClient.output.handle(key)`.\n- Event streams become `EventStreamClientHandle`s:\n  `await contractClient.events.subscribe(key, { onEvent, onGap?, onError? })`.\n- Live models become `LiveModelClientHandle`s:\n  `contractClient.conversation.state(key, 'state')` and\n  `contractClient.conversation.mutate('setTitle', envelope)`.\n- Jobs become `LiveJobClientHandle`s: `contractClient.build.start(input)`,\n  `contractClient.build.cancel(jobId)`, and `contractClient.build.handle(jobId)`.\n\n`LiveClientHandle` is the common live handle:\n\n```ts\ntype LiveClientHandle<T> = {\n  topic: string;\n  snapshot(): Promise<LiveSnapshot<T>>;\n  attach(\n    push: (update: LiveUpdate) => void,\n    options?: {\n      onReattach?: () => void;\n      onReattachError?: (error: WireError, context: { retrying: boolean }) => void;\n    }\n  ): Promise<Unsubscribe>;\n  asLiveSource(): LiveSource;\n};\n```\n\n`snapshot()` and `attach()` are useful for low-level code. For example, PTY output\ncan stream directly into an xterm instance without allocating another store:\n\n```ts\nconst output = contractClient.ptyOutput.handle({ sessionId });\nterm.write((await output.snapshot()).data.text);\n\nconst detach = await output.attach((update) => {\n  term.write((update.delta as { chunk: string }).chunk);\n}, {\n  onReattach: async () => {\n    term.reset();\n    term.write((await output.snapshot()).data.text);\n  },\n  onReattachError: (error, { retrying }) => {\n    if (!retrying) showOutputError(error.message);\n  },\n});\n```\n\nWhen a process wants local state, pass the client handle to a replica wrapper instead.\n\n## Forwarding\n\nContract clients are intentionally forwardable. Use `forwardController(contract, client)`\nwhen a hop should serve the same contract without owning state or intercepting\nindividual endpoints:\n\n```ts\nconst upstream = client(api, connect(sshTransport));\nconst controller = forwardController(api, upstream);\n```\n\nNo live state is created at that hop. The downstream client sees the same contract,\nwhile calls, snapshots, live attachments, jobs, and group mutations are forwarded to\nthe upstream connection. Event stream handles are forwarded as live attachments with\nthe same at-most-once delivery semantics as the upstream connection.\n\nForwarding also preserves upstream live lifecycle. A downstream subscriber receives\n`onGap` after the upstream attachment has reattached, and receives `onError` when\nan upstream reattach fails, even if the downstream transport never disconnected.\n\nThis is the right shape for protocol relays and middle tiers that should not own or\ncache state. The hop does not create `ReplicaState`s, does not allocate local\ncursor spaces, and does not run mutation settling. It just preserves the contract\nsurface while delegating to the upstream connection.\n\n`forwardController()` is the preferred shape for full protocol relays because it\nderives the controller implementation from the contract. Adding a new endpoint to\nthe contract does not require a hand-written forwarding entry at each relay. File\ndownloads are adapted internally from the client download handle back into the\ncontroller's streamed download shape.\n\nUse `createController()` directly when forwarding should be selective. A procedure\nmethod can be passed as a procedure implementation, and live client handles can be\npassed as live model, live log, event stream, or live job implementations:\n\n```ts\nconst upstream = client(workspaceApi, connect(sshTransport));\n\nconst controller = createController(workspaceApi, {\n  // Local interception.\n  ping: async () => 'desktop-main',\n\n  // Forwarded subtree.\n  git: upstream.git,\n\n  // Cached group; see Replicas.\n  conversation: createLiveModelReplicaCache(\n    workspaceApi.conversation,\n    upstream.conversation,\n    { lingerMs: 10 * 60_000 }\n  ),\n});\n```\n\nUse this composition style for middle tiers that need local handlers, auditing,\nauthorization checks, replicas, or other endpoint-specific behavior. The explicit\ncontroller implementation makes the interception points visible.\n\n## When to Use Replicas\n\nUse replicas when a process wants local state. See [Replicas](../live/replicas.md).\n\nCommon examples:\n\n- A renderer window wants observable UI state.\n- Electron main wants state that survives renderer reloads.\n- A process wants a local cache before serving the same live model contract to\n  downstream clients.\n\nFor Electron main and other middle tiers that both serve and inspect live state, use\nthe replica local access pattern in\n[Replicas](../live/replicas.md#model-replicas).\n\nDo not materialize just to forward. Forward the client handle or subtree directly.\n\n<!-- File: packages/wire/docs/api/contracts.md (Tokens: ~1859 | Category: anthropic-skill) -->\n# API Contracts\n\nContracts describe the API surface once. Controller creation and client creation\nderive their types from the same object.\n\n## Defining a Contract\n\nUse `defineContract({ ... })`. Object keys are significant: they determine\nprocedure paths and live ref ids after nested contracts are mounted.\n\n```ts\nexport const notesApi = defineContract({\n  activity: liveLog({ key: sessionKeySchema }),\n  session: liveModel({\n    key: sessionKeySchema,\n    states: {\n      notes: liveState({ data: notesStateSchema }),\n    },\n    mutations: {\n      addNote: mutation(\n        { input: z.object({ text: z.string() }), data: noteSchema, error: z.string() },\n        (ctx, input) => {\n          const note = { id: input.text.toLowerCase(), text: input.text };\n          ctx.produce('notes', (draft) => {\n            draft.notes.push(note);\n          });\n          return ok(note);\n        }\n      ),\n    },\n  }),\n  clearNotes: procedure({\n    input: sessionKeySchema,\n    output: notesStateSchema,\n  }),\n});\n```\n\nSee [../../examples/api-definition/contract.ts](../../examples/api-definition/contract.ts).\n\n## Endpoint Kinds\n\n### `procedure`\n\n`procedure({ input, output })` defines a request/response call. Procedure clients\naccept an optional `{ signal }` call option for cooperative cancellation; see\n[serving](./serving.md#cancellation).\n\n### `fallible`\n\n`fallible({ input, data, error })` defines a procedure whose output is a\n`Result<data, error>` payload:\n\n```ts\nloadNote: fallible({\n  input: z.object({ id: z.string() }),\n  data: noteSchema,\n  error: z.object({ type: z.literal('note_not_found') }),\n});\n```\n\nUse `fallible()` for expected domain failures that callers should handle as data.\nThrown `WireError`s are reserved for infrastructure failures and bugs such as\ndisconnects, unknown paths, and uncaught handler exceptions. See\n[wire errors](./errors.md).\n\n### `mutation`\n\n`mutation({ input, data, error }, handler?)` defines a live-state mutation shape.\nMutations are members of `liveModel()` definitions. Optimistic previews should use\nshared reducer functions with the `optimistic()` bridge rather than contract-local\nclient models.\n\n`mutation()` is for operations that must settle live model cursors. Use\n`procedure()` for calls that do not update live models and `liveJob()` for\nlong-running work.\n\n### `liveLog`\n\n`liveLog({ key })` declares a keyed text log endpoint. It is served by a\n`LiveLogSource` resolver and consumed through a `LiveLogClientHandle` or a\n`LiveLogReplicaCache`.\n\n### `eventStream`\n\n`eventStream({ key, event })` declares a keyed, fire-and-forget event channel.\nEvents are delivered only while a client is attached; the server keeps no\nretained snapshot and late subscribers do not receive historical events.\n\n```ts\nfileEvents: eventStream({\n  key: z.object({ rootPath: z.string() }),\n  event: z.object({\n    kind: z.enum(['create', 'update', 'delete']),\n    path: z.string(),\n  }),\n});\n```\n\nServe event streams with `createEventStreamHost(contract.fileEvents)` or a\nresolver that returns an `EventStreamSource`. Client handles expose\n`await subscribe(key, { onEvent, onGap?, onError? })`. `onGap` runs after\ntransport reattach so consumers can resync state that may have missed events.\nUse `eventStream` for loss-tolerant notifications and invalidation signals; use\n`liveLog` when callers need retained text output and `liveModel` when callers\nneed convergent state. See [event streams](../live/event-stream.md).\n\n### `liveJob`\n\n`liveJob({ input, progress, result, error })` models long-running work with progress,\ncancellation, terminal state, and reattach:\n\n```ts\nbuild: liveJob({\n  input: z.object({ target: z.string() }),\n  progress: z.object({ step: z.string() }),\n  result: z.object({ artifact: z.string() }),\n  error: z.object({ message: z.string() }),\n});\n```\n\n`createController()` binds the endpoint to `{ run, toError? }`. `run` returns a\n`Result<result, error>` payload; `toError` is only an optional fallback for\nunexpected thrown errors. The typed client gets `start(input)`, `cancel(jobId)`,\nand `handle(jobId)` helpers. See\n[live jobs](../live/live-job.md).\n\n### `downloadFile` and `uploadFile`\n\n`downloadFile({ input, meta?, error })` and\n`uploadFile({ input, accept?, maxSize?, result, error })` model byte transfer as a\ntyped call envelope plus a credit-based blob channel. Bytes are streamed in\nbounded chunks and are not embedded in procedure inputs or results:\n\n```ts\nreadAttachment: downloadFile({\n  input: z.object({ id: z.string() }),\n  error: z.object({ type: z.string() }),\n});\n\nuploadAttachment: uploadFile({\n  input: z.object({ conversationId: z.string() }),\n  accept: ['image/png', 'image/jpeg'],\n  maxSize: 25 * 1024 * 1024,\n  result: attachmentRefSchema,\n  error: z.object({ type: z.string() }),\n});\n```\n\nDownload clients receive a single-use handle with `chunks()`, `bytes()`, `file()`,\nand `cancel()`. Upload clients pass a file-like value as the second argument. See\n[file endpoints](./files.md).\n\n### `liveModel`\n\n`liveModel({ key, states, mutations })` aggregates related live states and the\nmutations that may touch them. A single key addresses every member. States are\ndeclared with `liveState({ data })`; the helper creates keyed member refs.\n\n```ts\nconst api = defineContract({\n  conversation: liveModel({\n    key: conversationKeySchema,\n    states: {\n      state: liveState({ data: stateSchema }),\n      usage: liveState({ data: usageSchema }),\n    },\n    mutations: {\n      setTitle: mutation(\n        { input: z.object({ title: z.string() }), data: z.void(), error: z.string() },\n        (ctx, input) => {\n          ctx.produce('state', (draft) => {\n            (draft as { title: string }).title = input.title;\n          });\n          ctx.produce('usage', (draft) => {\n            (draft as { tokens: number }).tokens += input.title.length;\n          });\n          return ok(undefined);\n        }\n      ),\n    },\n  }),\n});\n```\n\nInline group mutation handlers should be pure functions of the member drafts and\ninput: avoid I/O, time, randomness, and server-only state in the inline handler\nbody. If a mutation is schema-only (`mutation(def)`), the server supplies the\nhandler when it creates the live model host.\n\n## Nested Composition\n\nContracts can contain other contracts:\n\n```ts\nconst ptyAgent = defineContract({\n  sessions: liveModel({\n    key: sessionKeySchema,\n    states: { state: liveState({ data: sessionStateSchema }) },\n  }),\n  output: liveLog({ key: sessionKeySchema }),\n});\n\nconst api = defineContract({ ptyAgent });\n```\n\nThe final mount path determines ids and procedure paths:\n\n- `api.ptyAgent.sessions.states.state.id` is `ptyAgent.sessions.state`.\n- `api.ptyAgent.output.id` is `ptyAgent.output`.\n- a procedure at `api.ptyAgent.startSession` is called as `ptyAgent.startSession`.\n- a group member at `api.tasks.conversation.states.state` gets the id\n  `tasks.conversation.state`.\n\nThis lets packages define small contracts locally and compose them into a larger\nworkspace API without an extra namespace argument.\n\n## Live Model Providers\n\nOn the server, implement a live model endpoint with a `LiveModelProvider`. The\nusual way to build one is `expose()` from `@emdash/wire/state`, which resolves\neach contract state to a kernel readable per key:\n\n```ts\nconst conversations = expose(\n  api.conversation,\n  {\n    state: (key) => conversationState(key),\n    usage: (key) => conversationUsage(key),\n  },\n  { mutations }\n);\n```\n\nThen bind the endpoint by passing the provider in the implementation object:\n\n```ts\nconst controller = createController(api, { conversation: conversations });\n```\n\nThe live model client handle exposes `state(key, name)` for member handles and\n`mutate(name, envelope)` for mutation calls. Wrap it in `createLiveModelReplicaCache()`\nwhen a process wants local state, ref counting, and mutation settling. See\n[serving](./serving.md#typed-clients).\n\n<!-- File: packages/wire/docs/api/errors.md (Tokens: ~1165 | Category: anthropic-skill) -->\n# Wire Errors\n\nWire has two error planes:\n\n- Expected domain failures travel as `Result` payloads. Prefer `fallible()` for\n  procedure contracts whose callers should handle failures as ordinary data.\n- Thrown `WireError`s are reserved for infrastructure failures, contract drift,\n  lifecycle mistakes, and uncaught handler exceptions.\n\n`WireError` instances carry a typed `code`, a message, and an optional serialized\ncause. When an uncaught JavaScript error crosses the wire, the transport serializes\nit as `HANDLER_ERROR` with a clone-safe cause.\n\n## Codes\n\n### `CANCELLED`\n\nThe call was aborted. The client raises it when the caller's `AbortSignal` is\nalready aborted or aborts while a call is pending. The server raises it when it\nreceives a `cancel` message and the handler observes the abort.\n\nRetrying is not automatic and usually not appropriate because cancellation was\ncaller intent.\n\n### `DISCONNECTED`\n\nThe transport disconnected while a request was in flight, posting a request\nfailed because the peer was already gone, the held-call buffer overflowed while\ndisconnected, or the transport failed permanently (the terminal cause is\nattached as `cause`).\n\nMutations are never retried automatically. Callers can opt in per call with\n`retry: { schedule }`; that retry is safe because mutations carry a stable\n`mutationId` and the server deduplicates repeated ids.\n\n### `UNKNOWN_PROCEDURE`\n\nNo procedure handler exists for the call path. Common causes are an incorrect\nclient path, a missing implementation branch in `createController()`, or a drift\nbetween the contract and the controller implementation.\n\nThis is not retryable without changing code or wiring.\n\n### `UNKNOWN_TOPIC`\n\nThe live topic's ref id is not registered by the controller.\n\nThis usually means the client encoded the wrong live ref id, or a controller was\ncreated without the endpoint that owns the ref. It is not retryable without\nchanging the topic or server wiring.\n\n### `NOT_FOUND`\n\nThe live ref id is known, but no live instance exists for the provided key.\nExamples include a live model group instance that has not been created yet or a\njob state that has already been evicted.\n\nRetrying may make sense only if the domain resource is expected to appear later.\n\n### `MISSING_HANDLER`\n\nController creation-time error: a contract endpoint has no implementation. Examples include a\nmissing procedure implementation, missing live resolver, missing job handler, or\nmissing live model provider.\n\nThis should not cross the wire during normal operation. Fix the controller implementation.\n\n### `CONTRACT_MISMATCH`\n\nController creation-time error: a live model provider, replica cache, or client\nhandle was created for a different contract than the endpoint being bound.\n\nThis is not retryable; fix the provider/contract pairing.\n\n### `ALREADY_EXISTS`\n\nA keyed live resource was created for a key that already has a live instance.\n\nThis is a lifecycle error. Dispose or reuse the existing instance instead of\ncreating another one for the same key.\n\n### `TIMEOUT`\n\nA Wire call or snapshot exceeded an infrastructure deadline. The connection\napplies a default 30s call deadline (`callTimeoutMs`, per-call `timeoutMs`\noverride) spanning call-issued to result-received, including time spent held\nwhile disconnected; live attach traffic and blob streaming are exempt. Timeout\nis distinct from `CANCELLED`: cancellation is caller intent, while timeout means\npolicy stopped waiting for the operation.\n\nTimeouts are not retried automatically. Mutation calls that opt in to\n`retry: { schedule }` retry both `DISCONNECTED` and `TIMEOUT`, relying on\nstable `mutationId` deduplication. If a domain treats a timeout as an expected\noutcome, model it as a `Result` payload instead of relying on the\ninfrastructure error plane.\n\n### `HANDLER_ERROR`\n\nAn uncaught exception escaped a server handler or validation boundary. Expected\ndomain failures should be modeled as `Result` payloads with `fallible()` instead\nof thrown.\n\nThe serialized wire error includes the thrown value as a clone-safe cause. Treat\nthis as a bug signal and fix the handler or validation logic.\n\n## Live Attachment Errors\n\nLive attachments use the same infrastructure error plane as calls and snapshots.\nThey do not wrap startup or lifecycle failures in endpoint `Result` payloads.\n\nInitial attachment failures reject the `attach()` or event-stream `subscribe()`\npromise with a `WireError`. Common codes are `DISCONNECTED`, `UNKNOWN_TOPIC`,\n`NOT_FOUND`, and `HANDLER_ERROR`.\n\nOnce an attachment is established, the client keeps it as durable intent across\ntransport reconnects. Reattach failures are delivered to the subscription's\n`onReattachError` or `onError` callback with `{ retrying }`:\n\n- `DISCONNECTED` is reported with `retrying: true`; the attachment stays\n  registered and will try again on the next reconnect.\n- Other `WireError`s are reported with `retrying: false`; the attachment is\n  terminated because retrying the same topic would repeat the same failure.\n\nForwarding preserves these lifecycle notifications. A downstream connection can\nremain healthy while an upstream live attachment reports a gap or terminal error.\nForwarding sends those as live lifecycle messages rather than hiding them behind\nthe downstream transport state.\n\nThese errors are distinct from expected domain events carried by a stream. For\nexample, fs-watch may emit `{ kind: 'error' }` as part of its event payload when a\nnative watcher fails to start. That payload belongs to the fs-watch contract. A\nlive attachment `WireError` means the wire topic itself could not attach or\nreattach.\n\n<!-- File: packages/wire/docs/api/files.md (Tokens: ~678 | Category: anthropic-skill) -->\n# File Endpoints\n\n`downloadFile()` and `uploadFile()` stream bytes through a dedicated blob channel\ninstead of embedding `Uint8Array` payloads in procedure inputs or results. The call\npayload carries only typed input, metadata, and a channel id; byte chunks flow after\nthat over credit-based frames.\n\n## Contracts\n\n```ts\nconst api = defineContract({\n  readAttachment: downloadFile({\n    input: z.object({ id: z.string() }),\n    error: z.object({ type: z.string() }),\n  }),\n  uploadAttachment: uploadFile({\n    input: z.object({ conversationId: z.string() }),\n    accept: ['image/png', 'image/jpeg'],\n    maxSize: 25 * 1024 * 1024,\n    result: attachmentRefSchema,\n    error: z.object({ type: z.string() }),\n  }),\n});\n```\n\nFile metadata uses `{ name, mimeType, size?, lastModified? }`. Download endpoints\nmay extend the metadata schema with endpoint-specific fields.\n\n## Server Implementations\n\n```ts\nconst controller = createController(api, {\n  readAttachment: async ({ id }) => {\n    const stored = await store.get(id);\n    if (!stored) return err({ type: 'missing' });\n    return ok({\n      meta: {\n        name: stored.ref.name,\n        mimeType: stored.ref.mimeType,\n        size: stored.data.byteLength,\n      },\n      source: stored.stream(),\n    });\n  },\n  uploadAttachment: async (input, file) => {\n    const bytes = await file.bytes();\n    return ok(await store.put({ data: bytes, mimeType: file.mimeType, name: file.name }));\n  },\n});\n```\n\nThe source can be an `AsyncIterable<Uint8Array>` or a readable stream-like value.\n`WireFile` on upload exposes `stream()`, `bytes()`, `file()`, and `cancel()`.\n\n## Client Usage\n\n```ts\nconst downloaded = await client.readAttachment({ id });\nif (downloaded.success) {\n  for await (const chunk of downloaded.data.chunks()) {\n    // incremental processing\n  }\n}\n\nconst uploaded = await client.uploadAttachment({ conversationId }, file);\n```\n\nDownload handles are single-consumption: choose `chunks()`, `bytes()`, or `file()`.\n`bytes()` verifies `meta.size` when present.\n\n## Protocol Invariants\n\n- Bytes are sent as `blob-chunk` frames and never inside `call`/`result` values.\n- Producers send only after the consumer grants credit with `blob-pull`.\n- Consumers keep a bounded local chunk mailbox aligned with the maximum granted\n  credit. Overflow is treated as a protocol failure; blob chunks are never\n  dropped.\n- `blob-end` is the only successful EOF signal; disconnect before it is an error.\n- `blob-close` cancels a channel and disposes the producer source.\n- Frames for unknown channels are ignored so close/chunk races are harmless.\n- Chunks are re-sliced to standalone buffers before posting to avoid cloning a much\n  larger backing `ArrayBuffer`.\n\nStructured-clone transports carry `Uint8Array` chunks natively. `streamTransport`\nuses length-prefixed binary framing so workspace-server stdio/socket/SSH paths send\nraw bytes with no base64 expansion.\n\n<!-- File: packages/wire/docs/api/middleware.md (Tokens: ~1290 | Category: anthropic-skill) -->\n# Composable Middleware\n\nWire uses explicit composition for execution policy. Contracts describe protocol\nshape; middleware wraps handlers or controllers at the composition site.\n\n```ts\nimport { compose, deduplicate, withRetry, withTimeout } from '@emdash/shared/requests';\nimport { retrySchedules } from '@emdash/shared/scheduling';\nimport { createController } from '@emdash/wire/rpc';\n\nconst loadStats = compose(\n  async (input: { repo: string }, meta: { signal?: AbortSignal }) => {\n    return await fetchStats(input.repo, { signal: meta.signal });\n  },\n  [\n    withTimeout({ timeoutMs: 20_000 }),\n    withRetry({\n      schedule: retrySchedules.exponential({\n        initialMs: 250,\n        maxMs: 2_000,\n        maxRetries: 3,\n      }),\n      shouldRetry: isTransient,\n    }),\n    withTimeout({ timeoutMs: 5_000 }),\n  ]\n);\n\nconst controller = createController(api, { loadStats });\n```\n\n## `compose()`\n\n`compose(target, middlewares)` accepts the value being wrapped first and a\nreadonly middleware array second:\n\n```ts\ntype Middleware<T> = (next: T) => T;\n```\n\nThe first array entry is outermost. It sees the request first and settles last:\n\n```mermaid\nflowchart LR\n  composeCall[\"compose(handler, middlewares)\"] --> totalTimeout[TotalTimeout]\n  totalTimeout --> retry[Retry]\n  retry --> attemptTimeout[AttemptTimeout]\n  attemptTimeout --> handler[RawHandler]\n```\n\nThis order makes policy boundaries explicit. In the first example, the outer\ntimeout bounds the complete retry program, while the inner timeout bounds each\nindividual attempt.\n\n## Handler Middleware\n\nHandler middleware wraps procedure implementations before `createController()`:\n\n```ts\nconst handler = compose(rawHandler, [\n  withTimeout({ timeoutMs: 10_000 }),\n  deduplicate({ key: (input) => input.repo }),\n]);\n```\n\nUse handler middleware for endpoint-specific execution policy:\n\n- Timeout and deadline policy.\n- Retry policy for idempotent or externally safe operations.\n- In-flight request deduplication.\n- Per-endpoint rate limiting or concurrency limits when those utilities exist.\n\nProcedure handlers receive `(input, meta)`. Middleware must preserve every field\non `meta` and replace only `meta.signal` when it needs derived cancellation:\n\n```ts\nconst wrapped = async (input, meta) => {\n  return await next(input, { ...meta, signal: derivedSignal });\n};\n```\n\nMiddleware functions are intentionally not contract annotations. Two endpoints can\nshare the same schema and still have different execution policy depending on\nwhere they are served.\n\n## Controller Middleware\n\nController middleware wraps a complete `Controller` and satisfies\n`Middleware<Controller>` from `@emdash/shared/requests`. The seam exists as the\nsanctioned extension point for future cross-cutting concerns (for example\nobservability), even though Wire ships no controller middleware today.\n\nContract validation is not middleware: it is a `createController()` option.\nEvery controller validates by default — `'full'` (inputs + outputs) outside\nproduction, `'inputs'` in production — with an explicit `validate` override:\n\n```ts\nconst controller = createController(api, impl, { validate: 'inputs' });\n```\n\nUse controller middleware for boundary-wide behavior:\n\n- Logging and request instrumentation.\n- Session or authorization checks that apply to every procedure on the served\n  controller.\n\nPrefer handler middleware when the policy belongs to one endpoint. Prefer\ncontroller middleware when the policy belongs to the served boundary.\n\n## Timeout\n\n`withTimeout({ timeoutMs, clock? })` from `@emdash/shared/requests` is handler\nmiddleware backed by Shared `runWithTimeout()` from `@emdash/shared/scheduling`.\nIt derives a child `AbortSignal`, passes that signal to the wrapped handler, and\nconverts an expired deadline to a timeout error.\n\n```ts\nconst startSession = compose(rawStartSession, [\n  withTimeout({ timeoutMs: 10_000, clock }),\n]);\n```\n\nTimeout is infrastructure failure, not a domain result. If callers should handle\na timeout as expected data, model it in the endpoint's `Result` error schema\ninstead.\n\nCaller cancellation still wins over timeout. If the caller aborts the request,\nthe wire call rejects with `CANCELLED`.\n\n## Retry\n\n`withRetry()` from `@emdash/shared/requests` is handler middleware backed by\nShared `retry()` from `@emdash/shared/scheduling`. It requires an explicit\n`shouldRetry` classifier:\n\n```ts\nconst loadRemote = compose(rawLoadRemote, [\n  withRetry({\n    schedule: retrySchedules.fixed(250, 2),\n    shouldRetry: (error) => isWireError(error, 'DISCONNECTED'),\n  }),\n]);\n```\n\nDo not retry arbitrary procedure failures. A retry policy should be attached only\nwhen the operation is idempotent, deduplicated by request identity, or otherwise\nsafe to repeat. Contract mutations already have their own retry behavior and\nretry only `DISCONNECTED` by default.\n\n## Deduplication\n\n`deduplicate(options?)` from `@emdash/shared/requests` is handler middleware for\nsharing one in-flight execution for identical inputs:\n\n```ts\nconst expensiveStats = compose(rawExpensiveStats, [\n  deduplicate({ key: (input) => `${input.repo}:${input.branch}` }),\n]);\n```\n\nThe default key is stable JSON identity. Settled calls are not cached and\nrejections are not cached.\n\nCaller cancellation is independent from shared execution. One caller may stop\nwaiting without aborting the underlying request for other waiters. Set\n`cancelWhenUnused: true` only when it is correct to abort shared work after the\nfinal waiter leaves.\n\n## What Stays Out Of Middleware\n\nKeep protocol-visible behavior in contracts:\n\n- Endpoint kind.\n- Input and output schemas.\n- Upload size and MIME constraints.\n- Live model, log, stream, and job semantics.\n- Mutation idempotency envelopes.\n\nKeep UI presentation policy, such as command visibility or context keys, outside\nWire middleware. Keep domain failures in `Result` payloads when callers are\nexpected to handle them.\n\n<!-- File: packages/wire/docs/api/serving.md (Tokens: ~2776 | Category: anthropic-skill) -->\n# Serving and Clients\n\nThe API layer turns a contract into a server-side `Controller`, serves it over a\n`WireTransport`, and creates a typed client over the matching transport.\n\n## Creating a Controller\n\n`createController(contract, impl)` maps each endpoint to server behavior:\n\n```ts\nconst controller = createController(\n  notesApi,\n  {\n    session: sessionsHost,\n    activity: () => activityLogServer,\n    clearNotes: (input) => {\n      instance.states.notes.produce((draft) => {\n        draft.notes = [];\n      });\n      return instance.states.notes.snapshot().data;\n    },\n  }\n);\n```\n\nThe `impl` object is keyed by the contract shape. Procedures receive\n`(input, meta)`. Jobs use `{ run, toError? }`, a `LiveJobClientHandle`, or a\n`LiveJobReplicaCache`. Live logs use resolver functions, `LiveLogClientHandle`s, or\n`LiveLogReplicaCache`. Event streams use `createEventStreamHost()`, resolver functions,\nor `EventStreamClientHandle`s. Live model contracts use `expose()` or an existing\n`LiveModelClientHandle`.\n\nMutation idempotency is configured on `expose()`, not on the\ncontroller. The controller only routes calls, snapshots, attachments, and live\nmutation procedure envelopes.\n\nLive model state is separate from the contract because instances are runtime\nresources. A controller can be created once, while conversations, sessions, or\nwindows create and dispose keyed state through `family()` and scoped `expose()`\nlifecycles.\n\nSee the state bridge tests under `src/state/bridge/`.\n\n## Serving\n\n`serve(transport, controller, options?)` listens for protocol messages:\n\n- `call` invokes `controller.call(path, input, meta)`.\n- `snapshot` calls `LiveSource.snapshot()`.\n- `attach` subscribes to a live source and forwards `update` messages.\n- `detach` unsubscribes.\n- `cancel` aborts an in-flight call by id.\n\n```ts\nconst pair = memoryTransportPair();\nconst stop = serve(pair.right, controller, {\n  logger,\n  instrumentation: { callEnd: (event) => logger.debug('call finished', event) },\n});\n```\n\n`serve()` returns an unsubscribe. Call it when the transport or server session\ngoes away. It also aborts in-flight calls and detaches live subscriptions when\nthe transport disconnects or when the serve loop is disposed.\n\n## Validation\n\nContract validation is built into `createController()`. Every controller\nvalidates by default, and the default is environment-sensitive: `'full'`\n(inputs + outputs) everywhere except production, `'inputs'` in production\n(`NODE_ENV === 'production'`). Pass `validate` to override:\n\n```ts\nconst controller = createController(notesApi, impl, { validate: 'inputs' });\nconst stop = serve(pair.right, controller);\n```\n\nPolicies:\n\n- `none`: no schema parsing.\n- `inputs`: parse procedure inputs, upload/download inputs, live model mutation\n  envelopes, job start/cancel inputs, and live topic keys before delegating.\n- `full`: includes `inputs`, then also parses procedure outputs, upload/download\n  results, mutation results, and live job start results.\n\nThe default rule applies identically to bare controllers, component instances,\ncomponent workers, and tests (`createTestWire`), so a contract violation in a\ntest fails the same way it would in dev. Production boundaries stay on\n`inputs`: values crossing a trust boundary are always parsed, while output\nvalidation is skipped where it is too expensive.\n\nValidation covers request/response values that pass through\n`Controller.call()` and live topic keys passed to `Controller.resolveLive()`.\nLive job progress, result, error values, and event stream payloads are emitted\nlater as live updates and are not intercepted.\n\n## Connecting\n\n`connect(transport, options?)` creates a low-level `Connection`:\n\n```ts\nconst connection = connect(pair.left, { instrumentation });\n```\n\n`Connection` supports:\n\n- `call(path, input, { signal? })`.\n- `snapshot(topic)`.\n- `attach(topic, push, { signal?, onReattach? })`.\n- `onDisconnect(cb)`.\n- `dispose()` to release the logical connection without closing its transport.\n\nOn disconnect, pending calls reject with `WireError` code `DISCONNECTED`.\nExisting attachments are retained locally. If the transport exposes\n`onReconnect`, `connect()` re-issues active `attach` requests after the replacement\nlink is live and then calls each attachment's `onReattach` callback.\n\nReplicas use `onReattach` for live models, logs, and jobs to force a fresh\nsnapshot after reattach. Direct client-handle consumers can use the same callback when\nthey need to reseed UI state after reconnect.\n\nAn attachment signal owns establishment for only that caller. Aborting it before readiness cancels\nthe pending attach when no other caller still needs the topic, while shared callers remain attached.\nAfter `attach()` resolves, the returned unsubscribe owns the established interest.\n\n`dispose()` rejects pending calls, releases live attachments and blob channels, and\nunsubscribes the connection from transport events. It intentionally does not call\n`transport.close()`. This allows a short-lived application handshake connection to\nvalidate a candidate transport before handing that same transport to a stable\nreconnecting connection.\n\nThe protocol layer intentionally has no version handshake. Receivers validate the\nmessage `kind` and required fields in `isWireMessage()`; unknown message kinds are\nignored by transport adapters that parse untrusted frames.\n\n## Typed Clients\n\n`client(contract, connection)` returns a `ContractClient` with the same nested\nshape as the contract:\n\n```ts\nconst contractClient = client(notesApi, connection);\n\nconst sessions = createLiveModelReplicaCache(notesApi.session, contractClient.session, {\n  onChange: {\n    notes: (state, meta) => {\n      console.log('notes model:', state, meta.kind);\n    },\n  },\n});\nconst lease = sessions.acquire({ sessionId: 'demo' });\nconst session = await lease.ready();\n\nconst added = await session.mutations.addNote({ text: 'Typed client mutation' });\nawait added.settled;\nawait lease.release();\nawait sessions.dispose();\n```\n\nLive model and live log accessors are client handles. Use `state(key, name)` or\n`handle(key)` to snapshot/attach without local state, or pass the client handle\nto a replica wrapper.\n\nMutations return `ContractMutationInvocation`:\n\n```ts\ntype ContractMutationInvocation<D, E> = {\n  result: LiveMutationResult<D, E>;\n  settled: Promise<void>;\n};\n```\n\n`MutationCallOptions` lets callers provide a `mutationId`, a per-call\n`timeoutMs` deadline override, and an explicit opt-in retry policy. There is no\nautomatic retry; retries reuse the mutation id, so opted-in retries stay\nidempotent:\n\n```ts\nawait session.mutations.addNote({ text: 'Optimistic title' }, {\n  mutationId: 'custom-mutation',\n  retry: { schedule: retrySchedule({ delaysMs: [250, 1_000], maxRetries: 2 }) },\n});\n```\n\n## Cancellation\n\nWire supports cooperative cancellation for request messages. The client sends a\nprotocol message for the request id:\n\n```ts\n{ kind: 'cancel', id: callId }\n```\n\nTyped procedure clients accept an optional `{ signal }` argument:\n\n```ts\nconst abort = new AbortController();\nconst result = client.slowOperation({ id: 'task' }, { signal: abort.signal });\n\nabort.abort();\nawait result; // rejects with WireError code CANCELLED\n```\n\nIf the signal is already aborted, the call rejects locally without posting.\nServer procedure handlers receive the same signal through `CallMeta`:\n\n```ts\nconst controller = createController(api, {\n  slowOperation: async (input, meta) => {\n    await abortableWork(input, meta.signal);\n    return { ok: true };\n  },\n});\n```\n\nCancellation is cooperative. Long-running handlers should pass the signal into\ntheir own async work, listen for `abort`, or periodically check\n`meta.signal?.aborted`. `snapshot` and `attach` requests also share the same server-side\ncancellation registry. Resourced event-stream activation receives an `AbortSignal` so it can release\npartially-created resources when the final pending attachment disappears.\n\nMutations are intentionally not cancellable through this API; they use\n`mutationId` for idempotency and retry. See [mutations](../live/mutations.md).\nCancelling a `liveJob().start` wire call does not cancel the spawned job; job\ncancellation is domain-level through the generated `<path>.cancel` procedure.\n\n## Composing Controllers\n\nMiddle tiers compose by creating a typed client for the upstream contract and\npassing client handles, local handlers, or replicas into a new controller. This\nkeeps forwarding typed and makes the chosen interception points explicit:\n\nIf the middle tier should only relay the entire upstream contract, use\n`forwardController(contract, client)` instead of spelling out every endpoint:\n\n```ts\nconst upstream = client(workspaceApi, connect(upstreamTransport));\nconst controller = forwardController(workspaceApi, upstream);\n```\n\nUse explicit `createController()` composition when the hop needs local behavior:\n\n```ts\nconst upstreamConnection = connect(upstreamTransport);\nconst upstream = client(workspaceApi, upstreamConnection);\n\nconst conversations = createLiveModelReplicaCache(workspaceApi.conversation, upstream.conversation, {\n  lingerMs: 10 * 60_000,\n});\n\nconst controller = createController(workspaceApi, {\n  // Local override.\n  ping: async () => 'desktop-main',\n\n  // Stateless forwarding.\n  git: upstream.git,\n\n  // Stateful interception/cache before serving downstream clients.\n  conversation: conversations,\n});\n```\n\nFor nested contracts, object destructuring is enough to assemble the\nimplementation shape:\n\n```ts\nconst controller = createController(appApi, {\n  ...upstream,\n  settings: {\n    ...upstream.settings,\n    save: async (input, meta) => {\n      await auditSettingsChange(input, meta.signal);\n      return upstream.settings.save(input, meta);\n    },\n  },\n});\n```\n\nUse replicas when the middle tier needs to inspect, cache, or transform live\nstate before exposing it downstream. Use client handles directly when the middle\ntier is only forwarding calls and live sources.\n\n## Multi-Window Sessions\n\n`createWireSessionHub(controller)` serves the same controller to multiple\ntransport sessions:\n\n```ts\nconst hub = createWireSessionHub(controller);\nconst pair = memoryTransportPair();\n\nhub.open('window-1', pair.right);\nconst contractClient = client(api, connect(pair.left));\n```\n\nOpening the same session id closes the previous transport. `close(id)` closes\none session and calls `transport.close?.()` after disposing the serve loop.\n`dispose()` is async: it closes all sessions and awaits `controller.dispose?.()`.\n\n`Controller.dispose`, when present, returns `Promise<void>`. Wrappers such as\nvalidation, logging, session hubs, tests, process runtimes, and Electron exposure\npropagate that promise so LiveJobSource servers and resource hosts can finish shutdown\nbefore their owner reports disposed.\n\n## Server-Side Call Helpers\n\nFor the full middleware pattern, including handler middleware versus controller\nmiddleware, see [composable middleware](./middleware.md).\n\n`compose(target, middlewares)` from `@emdash/shared/requests` applies target-first\nmiddleware arrays to handlers or controllers. The first array entry is outermost:\nit sees the request first and settles last.\n\n```ts\nconst loadStats = compose(\n  async (input, meta) => {\n    return await fetchStats(input.repo, { signal: meta.signal });\n  },\n  [\n    withTimeout({ timeoutMs: 20_000 }),\n    withRetry({ schedule, shouldRetry: isTransient }),\n    withTimeout({ timeoutMs: 5_000 }),\n  ]\n);\n\nconst controller = createController(api, { loadStats });\n```\n\nIn this example the outer timeout bounds the complete retry program, while the\ninner timeout bounds each attempt. Handler middleware must preserve the handler's\nshape. Procedure handlers receive `(input, meta)` and middleware should preserve\nall `meta` fields while deriving a replacement `meta.signal` when it needs\ncooperative cancellation.\n\n`deduplicate(options?)` from `@emdash/shared/requests` wraps procedure\nimplementations to share one in-flight promise for identical inputs:\n\n```ts\nconst controller = createController(api, {\n  expensiveStats: compose(\n    async (input, meta) => {\n      return await loadStats(input.repo, input.branch, { signal: meta.signal });\n    },\n    [deduplicate({ key: (input) => `${input.repo}:${input.branch}` })]\n  ),\n});\n```\n\nBehavior:\n\n- Default key is `stableStringify(input)`, so object property order does not\n  matter.\n- Only in-flight calls are deduplicated. Settled calls are not cached.\n- Rejections are not cached.\n- `meta.signal` is not part of the key and shared execution is not aborted by\n  one caller.\n- Do not wrap mutations; mutation idempotency is handled by `mutationId`.\n\nSee [../../examples/dedupe/server.ts](../../examples/dedupe/server.ts).\n\n<!-- File: packages/wire/docs/api/transports.md (Tokens: ~1627 | Category: anthropic-skill) -->\n# Transports\n\nA `WireTransport` is the protocol boundary:\n\n```ts\ntype WireTransport = {\n  post(message: WireMessage): void;\n  onMessage(cb: (message: WireMessage) => void): Unsubscribe;\n  onDisconnect(cb: () => void): Unsubscribe;\n  onReconnect?(cb: () => void): Unsubscribe;\n  onTerminalFailure?(cb: (error: unknown) => void): Unsubscribe;\n  close?(): void;\n};\n```\n\nThe same `serve()`, `connect()`, and `client()` code works across every\ntransport. Only construction changes.\n\n`onReconnect` is optional and fires whenever the transport establishes\nconnectivity, including the first time. Its presence marks the transport as\nreconnect-capable: `connect()` then holds calls issued while disconnected until\nthe next reconnect or the call deadline, and re-attaches live topics after each\nreconnect; replicas then force a fresh snapshot. Transports without it fail\ncalls immediately while disconnected. `onTerminalFailure` fires once when the\ntransport gives up permanently; `connect()` rejects all held and future calls\nwith a `WireError('DISCONNECTED')` carrying the cause. `close()` releases\nlisteners registered by the adapter. It closes the underlying channel only when\nthe adapter owns a closeable channel, such as a `MessagePort`.\n\n## Memory\n\n`memoryTransportPair()` creates paired in-process transports for tests and\nexamples:\n\n```ts\nconst pair = memoryTransportPair();\nserve(pair.right, controller);\n\nconst contractClient = client(api, connect(pair.left));\n```\n\n`pair.disconnect()` disconnects both sides. Each endpoint also exposes `close()`,\nwhich aliases the pair disconnect for test cleanup.\n\n`left` and `right` are the two ends of one duplex channel, not semantic roles.\nPosting on `left` delivers to listeners registered on `right`, and posting on\n`right` delivers to listeners registered on `left`. This mirrors\n`MessageChannel`'s `port1`/`port2`: tests commonly call `serve(pair.right, ...)`\nand `connect(pair.left)`, but the opposite assignment is equally valid.\n\n## DOM MessagePort\n\n`domPortTransport(port)` adapts browser `MessagePort` objects:\n\n```ts\nconst channel = new MessageChannel();\nserve(domPortTransport(channel.port1), controller);\n\nconst connection = connect(domPortTransport(channel.port2));\n```\n\nThe adapter calls `port.start?.()` and listens for `message` and `close` events.\n`close()` removes those listeners and calls `port.close?.()`.\n\n## Electron Windows\n\n`exposeWireToWindows()` serves one controller to many Electron renderer windows\nusing `MessageChannelMain`-style ports:\n\n```ts\nconst stop = exposeWireToWindows(\n  {\n    ipcMain,\n    createMessageChannel: () => new MessageChannelMain(),\n  },\n  controller,\n  { channel: 'wire' }\n);\n```\n\nThe renderer asks for a port, then waits for the browser-side transfer:\n\n```ts\nawait requestWirePort({ ipcRenderer, window }, { channel: 'wire' });\nconst port = await awaitWirePort(window, { channel: 'wire' });\nconst contractClient = client(api, connect(domPortTransport(port as MessagePort)));\n```\n\nOpening a new port for the same `webContents.id` closes the old one. Naturally\nclosed ports are removed from the session map. Internally, the helper uses\n`createWireSessionHub(controller)`, so session teardown also closes the transport.\n\n## Node Streams\n\n`streamTransport(input, output)` frames messages as newline-delimited JSON. It is\nuseful for subprocess, stdio, and SSH-style boundaries:\n\n```ts\nconst transport = streamTransport(child.stdout, child.stdin);\nconst contractClient = client(api, connect(transport));\n```\n\nMalformed frames are ignored. `close`, `end`, and `error` on the readable side\ntrigger disconnect listeners. `close()` stops parsing and clears local listeners;\nit does not close the readable or writable streams because those streams are owned\nby the caller.\n\n## Reconnecting\n\n`reconnectingTransport(connectOnce, options?)` wraps an async transport factory:\n\n```ts\nconst transport = reconnectingTransport(\n  async () => {\n    const pair = await openRemoteWirePair();\n    return pair.left;\n  },\n  { backoffMs: [100, 250, 500, 1000] }\n);\n\nawait transport.ready();\n```\n\n`connectOnce` is the readiness boundary for one physical connection. The returned\ntransport remains private until the promise resolves. Applications that require a\nhandshake should open the candidate, run the handshake against it, dispose the\ntemporary logical `Connection`, and only then return the candidate:\n\n```ts\nconst transport = reconnectingTransport(\n  async () => {\n    const candidate = await openRemoteTransport();\n    const handshakeConnection = connect(candidate);\n    const handshakeClient = client(api, handshakeConnection);\n    try {\n      const result = await handshakeClient.initialize({ version: PROTOCOL_VERSION });\n      if (!result.success) throw new ProtocolMismatchError(result.error);\n      return candidate;\n    } catch (error) {\n      candidate.close?.();\n      throw error;\n    } finally {\n      handshakeConnection.dispose();\n    }\n  },\n  {\n    shouldRetry: (error) => !(error instanceof ProtocolMismatchError),\n  }\n);\n\nawait transport.ready();\nconst stableClient = client(api, connect(transport));\n```\n\nThis ordering guarantees that held calls and live-topic reattachments cannot\novertake an application handshake. Wire itself remains version-agnostic; the\napplication owns the handshake contract and permanent-error classification.\n\nThe reconnecting transport does not queue messages. `post()` while no ready\ninner transport exists throws a `WireError('DISCONNECTED')`; robustness is\nowned entirely by `connect()`, which holds calls issued while disconnected\nuntil the next reconnect or the call deadline. When an inner transport\ndisconnects, listeners are notified, readiness resets, and reconnection starts.\n\n`ready()` resolves after the current `connectOnce` succeeds and its candidate is\ninstalled. After a disconnect, a new call to `ready()` waits for the replacement\ngeneration. A previously resolved readiness promise does not become pending\nagain, so disconnect observers should call `ready()` after receiving the\ndisconnect event.\n\n`shouldRetry(error, { attempt, isReconnect })` classifies factory or handshake\nfailures. Returning `false`, or exhausting a finite `retrySchedule`, permanently\nstops that transport, rejects its current readiness promise, and fires\n`onTerminalFailure` with the terminal error. Future `post()` calls fail with the\nterminal error. By default, failures remain retryable under the configured\nschedule.\n\n`onReconnect` fires whenever an inner transport reaches readiness, including\nthe first one. `Connection` listens for that signal to flush held calls and\nre-issue active `attach` requests; replicas refresh their snapshots after\nreattach. `close()` stops the retry loop, closes the current inner transport if\npresent, fires `onTerminalFailure`, and clears listeners.\n\n## Composition Notes\n\n`reconnectingTransport()` accepts `clock` and `retrySchedule` options. The legacy\n`backoffMs` array is normalized to a repeat-last retry schedule. Closing the\ntransport synchronously aborts pending reconnect sleeps and disposes late\nconnections.\n\nElectron window exposure returns an async cleanup function. Await it when tearing\ndown a runtime host so session hubs and controllers finish disposal before the\nworker or window bridge is replaced.\n\nTransport composition is ordinary function wrapping, so order matters when\nstacking adapters around `reconnectingTransport()`. For semantic telemetry (one\nevent per logical call, snapshot, attachment, cancellation, resync, or mutation\ndedupe), attach a `WireInstrumentation`; see\n[observability](../observability.md).\n\n<!-- File: packages/wire/docs/live/event-stream.md (Tokens: ~975 | Category: anthropic-skill) -->\n# Event Streams\n\n`eventStream({ key, event })` exposes a keyed server-to-client event channel for\nloss-tolerant notifications. It reuses the wire live attachment protocol but does\nnot retain events or expose meaningful snapshots.\n\nUse event streams when subscribers can recover from missed events by resyncing\nfrom another source of truth, such as a file tree, cache, or database query. Use\n`liveLog` for retained append-only text and `liveModel` for convergent state.\n\n## Contract\n\n```ts\nconst api = defineContract({\n  fileEvents: eventStream({\n    key: z.object({ rootPath: z.string() }),\n    event: z.object({\n      kind: z.enum(['create', 'update', 'delete']),\n      path: z.string(),\n    }),\n  }),\n});\n```\n\nThe key addresses an event stream instance. The event schema types the\nclient-side `onEvent` callback and documents the payload shape.\n\n## Server\n\nMost servers use `createEventStreamHost()`:\n\n```ts\nconst fileEvents = createEventStreamHost(api.fileEvents);\n\nconst controller = createController(api, {\n  fileEvents,\n});\n\nfileEvents.emit(\n  { rootPath: '/repo' },\n  { kind: 'update', path: '/repo/package.json' }\n);\n```\n\n`emit()` is fire-and-forget. If no client is attached to that key, the event is\ndropped. When the last subscriber detaches, the host discards the keyed source.\n\nIf attachment presence owns an external resource, pass lifecycle hooks to the\nhost. `onActive` runs when the first subscriber attaches to a key, and `onIdle`\nruns when the last subscriber detaches:\n\n```ts\nconst resourceEvents = resourcedStream({\n  key: z.object({ rootPath: z.string() }),\n  event: z.object({ kind: z.string(), path: z.string() }),\n});\nconst fileEvents = createEventStreamHost(resourceEvents, {\n  onActive(key) {\n    void startWatcher(key);\n  },\n  onIdle(key) {\n    void stopWatcher(key);\n  },\n});\n```\n\nThis is useful for resources that should be recreated after reconnect. The wire\nclient automatically reattaches live topics after reconnect, so `onActive` runs\nagain in the new server process.\n\nWhen a successful attach must mean that the resource is ready, define the endpoint with\n`resourcedStream()` and provide `activate`. Activation receives a signal and resolves to its owned\ndisposer:\n\n```ts\nconst fileEvents = createEventStreamHost(api.fileEvents, {\n  async activate(key, signal) {\n    const watcher = await startWatcher(key, { signal });\n    return () => watcher.dispose();\n  },\n});\n```\n\nConcurrent subscribers share one activation. The host aborts pending activation when the final\nsubscriber leaves, disposes a late result, and keeps activation alive when another subscriber still\nowns the key.\n\nYou can also pass a resolver to `createController()` if another component owns\nthe source:\n\n```ts\ncreateController(api, {\n  fileEvents: (key) => sources.get(key.rootPath),\n});\n```\n\n## Client\n\n```ts\nconst unsubscribe = await client.fileEvents.subscribe(\n  { rootPath: '/repo' },\n  {\n    onEvent(event) {\n      console.log(event.kind, event.path);\n    },\n    onGap() {\n      void reloadFileTree();\n    },\n    onError(error, { retrying }) {\n      if (!retrying) showFileEventStreamError(error.message);\n    },\n    signal,\n  }\n);\n```\n\n`onGap` runs after the underlying transport reattaches. Events may have been lost\nwhile the link was disconnected or while a supervised subprocess restarted, so\nconsumers should refresh any derived state in that callback.\n\nInitial attachment does not call `onGap`; subscribers should perform their own\ninitial load before or after subscribing.\n\nThe `subscribe()` promise rejects if the initial attachment fails. After an\nattachment is established, `DISCONNECTED` reattach failures call `onError` with\n`retrying: true` and remain registered for a later reconnect. Non-retryable\nreattach errors call `onError` with `retrying: false` and terminate the\nattachment.\n\n## Semantics\n\n- Events are at-most-once and only delivered to currently attached clients.\n- No historical events are retained for late subscribers.\n- Event stream keys are validated by controller validation.\n- Event payloads are not validated on the push path, matching other live updates.\n- Forwarding preserves upstream gaps and reattach errors even when the downstream\n  transport remains connected.\n\n## Example\n\nSee [../../examples/event-stream/client.ts](../../examples/event-stream/client.ts)\nfor a runnable in-process example.\n\n<!-- File: packages/wire/docs/live/live-job.md (Tokens: ~1417 | Category: anthropic-skill) -->\n# Live Jobs\n\nLive jobs model async work as a live state machine. They are useful when callers\nneed both a final result and a stream of progress updates.\n\n## State Shape\n\n`liveJobStateSchema(progress, result, error)` creates a schema for:\n\n```ts\ntype LiveJobState<P, R, E> =\n  | { status: 'running'; startedAt: number; progress: P[]; progressCount: number }\n  | { status: 'succeeded'; startedAt: number; finishedAt: number; progress: P[]; result: R }\n  | { status: 'failed'; startedAt: number; finishedAt: number; progress: P[]; error?: E; cause?: SerializedError }\n  | { status: 'cancelled'; startedAt: number; finishedAt: number; progress: P[] };\n```\n\n`progress` is a retained ring of recent entries. `progressCount` is the total\nnumber of progress events emitted, including entries that may have rolled out of\nthe retained ring. Terminal states retain lifecycle timestamps and the most\nrecent progress entries, but they do not carry `progressCount`.\n\n## Server\n\n`LiveJobSource<I, P, R, E>` accepts a handler that returns `Result<R, E>`:\n\n```ts\nconst jobs = new LiveJobSource<Input, Progress, Result, ErrorState>(\n  async (input, ctx) => {\n    ctx.progress({ step: 'checkout' });\n    await build(input, ctx.signal);\n    ctx.progress({ step: 'package' });\n    return ok({ artifact: `${input.target}.zip` });\n  },\n  { toError: (error) => ({ message: error instanceof Error ? error.message : String(error) }) }\n);\n\nconst { jobId } = jobs.start({ target: 'desktop' });\n```\n\nOptions:\n\n- `generation`: optional fixed generation for every job state model.\n- `terminalRetainMs`: how long terminal state remains attachable. Default:\n  `LIVE_JOB_TERMINAL_RETAIN_MS` (5 minutes).\n- `idFactory`: custom job id factory, useful for deterministic tests.\n- `clock`: custom timestamp source, useful for deterministic tests.\n- `toError`: optional fallback for unexpected thrown errors. Domain failures\n  should be returned as `err(error)` from the handler.\n- `onRunStarted`, `onRunChanged`, and `onRunEvicted`: optional hooks for callers\n  that want to maintain their own job listing.\n\nHandlers receive `LiveJobContext`:\n\n- `ctx.progress(value)` appends retained progress and emits an update.\n- `ctx.signal` is an `AbortSignal`. If the signal is aborted, the job finishes\n  as `cancelled`.\n\nReturn `ok(result)` for successful completion and `err(error)` for expected\ndomain failures. If the handler throws and `toError` is configured, the thrown\nvalue is mapped into typed `error`; otherwise the failed state carries a\nserialized `cause`.\n\n`source(jobId)` returns a read-only live source for that job. `snapshot(jobId)` and\n`getState(jobId)` are convenience accessors. `cancel(id)` aborts a running job.\n`dispose()` is async: it aborts running jobs, waits for cooperative handlers to\nsettle, clears eviction timers, and removes all job state.\n\nTerminal job state is retained for `LIVE_JOB_TERMINAL_RETAIN_MS` (5 minutes) so\nlate clients can reattach by id. This retention is process-local, not durable.\n\n## Consumers\n\nConsumers normally reach jobs through the API layer. The contract client starts,\ncancels, and attaches to jobs without retaining local state:\n\n```ts\nconst { jobId } = await contractClient.build.start({ target: 'desktop' });\nconst handle = contractClient.build.handle(jobId);\n\nconst detach = await handle.attach((update) => {\n  console.log(update.delta);\n});\n\nawait contractClient.build.cancel(jobId);\ndetach();\n```\n\nUse `createLiveJobReplicaCache()` when a consumer wants a `result` promise, progress\ncallbacks, ref-counted attachment, and terminal-state retention. If the job fails,\n`ReplicaJob.result` rejects with `LiveJobFailedError`; if it is cancelled, it\nrejects with `LiveJobCancelledError`. Progress emitted by a seed is suppressed so\nreattaching clients do not replay old progress as fresh events.\n\n## Contract Endpoint\n\nThe API contract layer can expose a job directly:\n\n```ts\nconst api = defineContract({\n  build: liveJob({\n    input: z.object({ target: z.string() }),\n    progress: z.object({ step: z.string() }),\n    result: z.object({ artifact: z.string() }),\n    error: z.object({ message: z.string() }),\n  }),\n});\n```\n\nOn the server, implement the endpoint with `{ run, toError? }`:\n\n```ts\nconst controller = createController(api, {\n  build: {\n    run: async (input, ctx) => {\n      ctx.progress({ step: 'compile' });\n      return ok({ artifact: `${input.target}.zip` });\n    },\n    toError: (error) => ({\n      message: error instanceof Error ? error.message : String(error),\n    }),\n  },\n});\n```\n\nThe typed client exposes a live job client handle. Use it directly for\nlow-level forwarding, or wrap it in `createLiveJobReplicaCache()` when a consumer wants\nref-counted local state:\n\n```ts\nconst jobs = createLiveJobReplicaCache(api.build, contractClient.build, { lingerMs: 30_000 });\nconst lease = await jobs.start({ target: 'desktop' });\nconst handle = await lease.ready();\n\nhandle.onProgress((progress) => console.log(progress.step));\nconsole.log(await handle.result);\n\nawait lease.release();\n\nconst reattachedLease = jobs.acquire(handle.jobId);\nconst reattached = await reattachedLease.ready();\nconsole.log(await reattached.result);\nawait reattachedLease.release();\nawait jobs.dispose();\n```\n\n`ReplicaJob` exposes `jobId`, `ready`, `result`, `getState()`,\n`onProgress(cb)`, and `cancel()`. The `LiveJobReplicaCache` manager owns ref counting,\nsubscription sharing, and terminal-state retention.\n\n`LiveJobSource` accepts a `Clock` for deterministic start/finish timestamps and\nterminal retention eviction. Server disposal is awaitable: it cancels active job\nruns, waits for cooperative handlers to settle, and disposes retention timers\nbefore returning.\n\nCancellation at the procedure/wire level is documented in\n[serving](../api/serving.md#cancellation). Job cancellation is domain-level:\n`ReplicaJob.cancel()` calls the generated `<path>.cancel` procedure, which calls\n`LiveJobSource.cancel(jobId)`.\n\nSee [../../examples/job-contract/client.ts](../../examples/job-contract/client.ts).\n\n<!-- File: packages/wire/docs/live/live-log.md (Tokens: ~721 | Category: anthropic-skill) -->\n# Live Logs\n\nLive logs are append-only text streams with retained tail snapshots. They are a\ngood fit for terminal output, build logs, and agent process output where clients\nneed to attach late and still see recent context.\n\n## Server\n\n`LiveLogSource` stores a bounded text buffer. `append(chunk)` emits\n`{ chunk }` deltas to subscribers and updates the retained snapshot:\n\n```ts\nconst server = new LiveLogSource({ generation: 3000, maxBufferBytes: 12 });\n\nexport function appendLine(line: string): void {\n  server.append(`${line}\\n`);\n}\n```\n\nOptions:\n\n- `generation`: optional fixed generation, useful in tests.\n- `maxBufferBytes`: retained buffer size. The default is 1 MiB.\n\nSnapshots contain `LiveLogSnapshotData`:\n\n```ts\ntype LiveLogSnapshotData = {\n  baseOffset: number;\n  text: string;\n  truncated: boolean;\n};\n```\n\n`baseOffset` is the byte offset of `text` in the logical full log. When the\nserver drops old bytes because `maxBufferBytes` was exceeded, `truncated` is\n`true`. The server keeps at least the newest chunk even if it exceeds the byte\nlimit.\n\n`reseed()` starts a new generation and resets sequence to `0`. With no argument,\nit clears retained text and resets `baseOffset` to `0`; with retained snapshot\ndata, it seeds the buffer from that data.\n\n## Consumers\n\nConsumers normally reach logs through the API layer. The client handle is useful\nwhen you want to stream directly into a terminal or log viewer:\n\n```ts\nconst output = contractClient.activity.handle(session);\nconst snapshot = await output.snapshot();\nterm.write(snapshot.data.text);\n\nconst detach = await output.attach((update) => {\n  term.write((update.delta as { chunk: string }).chunk);\n});\n```\n\nUse `createLiveLogReplicaCache()` when a process wants a retained local buffer that can\nalso be served downstream. Without a custom sink, the replica keeps an eager\nbounded `LiveLogSource` buffer and `text()` remains readable. With a custom `LogSink`,\nthe sink owns storage; readable `LogStore`s add `text()`, while write-only sinks\ncan stream directly into xterm without a duplicate text buffer.\n\nThe replica's internal log follower tracks the followed generation/sequence\nplus the UTF-8 byte offset already written to its sink. On reconnect or explicit `refresh()`, it\nsplices from the retained snapshot when the generation still matches, appending\nonly missing bytes instead of resetting rendered output. Generation changes or\nevicted gaps still reset the sink from the retained snapshot.\n\nThe underlying protocol follower resyncs on update-before-seed, generation\nmismatch, sequence gap, or invalid log delta. Resync events use the same `resync`\ninstrumentation hook as live models.\n\n## API Layer Usage\n\nContracts expose logs with `liveLog({ key })`:\n\n```ts\nconst api = defineContract({\n  activity: liveLog({ key: sessionKeySchema }),\n});\n\nconst controller = createController(api, {\n  activity: () => activityLogServer,\n});\n\nconst activity = contractClient.activity.handle(session);\nconst detach = await activity.attach((update) => {\n  console.log((update.delta as { chunk: string }).chunk);\n});\n\ndetach();\n```\n\nThe API layer handles topic encoding, snapshots, attachment, and detachment.\n\nSee [../../examples/live-log/client.ts](../../examples/live-log/client.ts).\n\n<!-- File: packages/wire/docs/live/live-model.md (Tokens: ~973 | Category: anthropic-skill) -->\n# Live States and Protocol\n\nLive states are reactive JSON state containers. A server owns authoritative\nstate, emits ordered Immer patches, and clients apply those patches locally.\nWhen a client detects a gap, it refetches a snapshot and resumes from the new\ngeneration.\n\n## Protocol Terms\n\nThe shared protocol lives in `src/live/protocol`.\n\n- `LiveSnapshot<T>` is the full state at a cursor:\n  `{ generation, sequence, timestamp, data }`.\n- `LiveUpdate` is an ordered delta:\n  `{ generation, baseSequence, sequence, timestamp, delta, mutationIds? }`.\n- `LiveCursor` is `{ generation, sequence }` and identifies a point in a live\n  state stream.\n- `LiveSource` is the common server-side shape consumed by the API layer:\n  `snapshot()` plus `subscribe(cb)`.\n\n`generation` changes when the server calls `reseed()`. `sequence` increments for\neach effective patch in a generation. `baseSequence` must equal the client's\ncurrent sequence; otherwise the client has missed an update and resyncs from\n`snapshot()`.\n\n## Server\n\n`LiveStateSource<T>` owns one authoritative state object. Mutate it with\n`produce()`, not by mutating the original object:\n\n```ts\nconst server = new LiveStateSource<TaskListState>(\n  {\n    tasks: [{ id: 'task-1', title: 'Read the plan', done: false }],\n    filter: 'all',\n  },\n  1000\n);\n\nconst cursor = server.produce(\n  (draft) => {\n    draft.tasks.push({ id: 'task-2', title: 'Apply the first patch', done: false });\n  },\n  { mutationIds: ['example-add-task'] }\n);\n```\n\n`produce()` returns the cursor containing the change. If the mutator is a no-op,\nno update is emitted and the current cursor is returned. `snapshot()` deep\nclones the current state. `reseed(next?)` replaces the generation, resets\nsequence to `0`, optionally replaces state, and forces clients to resync on the\nnext observed update.\n\n## Consumers\n\nConsumers normally reach live states through a live model contract. Use the\ncontract client when you only need protocol access:\n\n```ts\nconst model = contractClient.conversation.state({ sessionId }, 'state');\nconst snapshot = await model.snapshot();\nconst detach = await model.attach((update) => {\n  console.log(update.delta);\n});\n```\n\nWrap that live state client handle in `createLiveModelReplicaCache()` when a process wants local\nstate, mutation settling, ref counting, or a downstream `LiveSource`; see\n[replicas](./replicas.md#model-replicas).\n\nThe package also has low-level protocol followers internally. They consume\nsnapshots and ordered updates, then resync when:\n\n- an update arrives before `seed()`.\n- `generation` differs from the local generation.\n- `baseSequence` differs from the local sequence.\n- Immer patch application throws.\n- schema validation fails in non-production builds.\n\nThe reported resync reasons are `generation`, `sequence-gap`, `patch-failed`,\nand `validation`. Schema validation is skipped when `NODE_ENV === 'production'`;\nthe generation and sequence checks are the primary correctness mechanism.\n\nEvery follower is constructed with a required resync failure policy that\ndecides what happens when the snapshot refetch itself fails. Wire ships two\ncanned policies: `resyncRetry({ schedule? })` retries on a bounded backoff\nuntil success or dispose, and `resyncMarkStale()` gives up while keeping\nstaleness observable (`stale` on the client) until the next update or reattach\ntriggers a fresh attempt. Production replicas use `resyncRetry`. Every failure\nfires the `resyncFailed` instrumentation event. `refresh()` resolves only when\nthe follower is fresh: concurrent calls coalesce onto the in-flight resync and\na trigger arriving mid-reseed re-runs the loop before resolution. Disposing a\nfollower-based client rejects all of its pending waiters — including\nwait-forever waiters (`timeoutMs <= 0`) — with a disposed error.\n\n## Cursor and Mutation Waiters\n\n`waitForCursor(cursor, timeoutMs?)` resolves when the client has reached a\ncursor. `waitForMutation(mutationId, timeoutMs?)` resolves when an applied update\ncontains the mutation id, or when a seed/resync lands because the snapshot is\nauthoritative.\n\nThese waiters are used by mutation settling; see [mutations](./mutations.md).\n\nSee [../../examples/state-kernel/client.ts](../../examples/state-kernel/client.ts)\nfor an end-to-end example.\n\n<!-- File: packages/wire/docs/live/mutations.md (Tokens: ~1155 | Category: anthropic-skill) -->\n# Mutations\n\nMutations connect API calls to live model updates. A mutation can update one\nmodel, many instances of one model, or several model refs. The key extra piece\nis the `mutationId`: it tags emitted `LiveUpdate`s so clients can prove their\nbound live models have observed the mutation.\n\n## Why Mutation IDs Exist\n\nAn RPC result only tells the caller that the server handler finished. It does\nnot prove every live subscription in the UI has applied the corresponding\npatches. Mutation ids bridge that gap:\n\n```ts\nserver.produce(\n  (draft) => {\n    draft.tasks.push({ id: 'task-2', title: 'Apply the first patch', done: false });\n  },\n  { mutationIds: ['example-add-task'] }\n);\n```\n\nThe update carries `mutationIds: ['example-add-task']`. A replica can resolve\n`waitForMutation('example-add-task')` when its local model applies that update.\n\n## Providers and Context\n\nLive model contract mutations run against a `LiveModelProvider` — most commonly\none produced by `expose()` from `@emdash/wire/state`, which bridges kernel state\nonto the endpoint. Mutation handlers update kernel cells and await the observed\nrevision so the returned cursor is settled:\n\n```ts\nconst sessions = expose(api.session, { metadata: (key) => metadataCell(key) }, {\n  mutations: {\n    async setTitle(ctx) {\n      const revision = metadataCell(ctx.key).update(\n        (previous) => ({ ...previous, title: ctx.input.title }),\n        { mutationIds: [ctx.mutationId] }\n      );\n      await ctx.observed('metadata', revision);\n      return ok({ title: ctx.input.title });\n    },\n  },\n});\n```\n\nKeys use `stableStringify()`, so object key order does not matter when providers\nand client bindings look up an instance. The mutation context is instance-bound:\n`ctx.key` identifies the addressed member and `ctx.observed(name, revision)`\nrecords the cursor of every touched member state. The wire result is:\n\n```ts\ntype LiveMutationResult<D, E> =\n  | { success: true; data: { data: D; cursors: LiveCursorEntry[] } }\n  | { success: false; error: E };\n```\n\nThe `data.data` value is the domain result. `data.cursors` tells the client which\nlive model bindings need to catch up.\n\n## Client Settling\n\n`LiveModelReplicaCache.acquire(key)` returns a `ReplicaInstance` for one group key. Its\nmutation methods call the live model client handle and then settle against the local\n`ReplicaState`s.\n\nGroup mutation methods return `{ result, settled }`:\n\n```ts\nconst sessions = createLiveModelReplicaCache(api.session, contractClient.session);\nconst lease = sessions.acquire({ sessionId: 'demo' });\nconst session = await lease.ready();\n\nconst added = await session.mutations.addNote({ text: 'Typed client mutation' });\nawait added.settled;\nawait lease.release();\nawait sessions.dispose();\n```\n\n`settled` waits for every cursor in the mutation result. For each cursor entry,\nit resolves when either:\n\n- the matching binding applies an update tagged with the mutation id, or\n- the matching binding reaches the returned cursor.\n\nThis lets UI code safely read live client snapshots after `await settled`.\n\n## Group Contract Mutations\n\nThe API layer integrates mutations through `liveModel()` member\nmutations. Each group mutation becomes a client method:\n\n```ts\nconst updated = await session.mutations.setTitle({ title: 'Grouped wire' }, {\n  mutationId: 'custom-mutation',\n});\nawait updated.settled;\n```\n\nIf no id is provided, the replica mutation helper generates one with\n`createMutationId()`.\nExplicit ids are useful for optimistic previews, where the preview and server\nmutation must share the same confirmation id.\n\n## Idempotency and Retries\n\nThe server-side idempotency cache used by `expose()` stores settled mutation\nresults by `mutationId` and shares one in-flight execution for concurrent\nduplicates.\n\nBy default the cache keeps entries for 5 minutes with a 1000-entry cap.\nConfigure or disable it through the `idempotency` option:\n\n```ts\nconst sessions = expose(api.session, states, {\n  mutations,\n  idempotency: { ttlMs: 60_000, maxEntries: 500 },\n});\n\nconst withoutDedupe = expose(api.session, states, { mutations, idempotency: false });\n```\n\nThe client never retries mutations automatically. Callers opt in per call with\nan explicit retry schedule; retries reuse the same `mutationId`, so the\nserver-side cache deduplicates them:\n\n```ts\nawait session.addNote(input, {\n  mutationId: 'add-note-1',\n  retry: { schedule: retrySchedule({ delaysMs: [250, 1_000], maxRetries: 2 }) },\n});\n```\n\nOpted-in retries fire only for `DISCONNECTED` and `TIMEOUT` errors; they never\nhappen for `CANCELLED` errors.\n\nThe cache is process-local and temporary. It provides at-most-once behavior\nwithin one server process lifetime, not durable exactly-once semantics. If a\nmutation has durable side effects such as database writes, store the\n`mutationId` in that domain layer too.\n\nUse `procedure()` for API calls that do not need live model cursor settling.\n`mutation()` is only valid as a member of `liveModel().mutations`\nin the contract API.\n\n<!-- File: packages/wire/docs/live/replicas.md (Tokens: ~1347 | Category: anthropic-skill) -->\n# Replicas\n\nReplicas are consumer-instantiated materialization wrappers around client handles from\n`client()`. The typed client itself never stores live state. A consumer chooses\none of four shapes:\n\n- Own state: `LiveStateSource`, `LiveLogSource`, `LiveJobSource`, or an\n  `expose()` provider over kernel state.\n- Stream directly: use a client `snapshot()`/`attach()` handle with no local store,\n  for example PTY output written straight into xterm.\n- Forward: pass client handles or subtrees to `createController()` so a hop stays stateless.\n- Replica: wrap a client handle to hold local state, share upstream subscriptions, and\n  serve downstream clients.\n\nEvery replica manager has the same lifecycle shape:\n\n```ts\nconst lease = replica.acquire(key);\nconst instance = await lease.ready();\nawait lease.release();\nawait replica.dispose();\n```\n\n`acquire()` increments the ref count for that key. The first lease opens the\nupstream subscription, concurrent leases share it, and the last release starts\nthe optional `lingerMs` timer. `peek(key)` returns a warm instance while it is\nretained.\n\n## Model Replicas\n\nLive models are exposed only through `liveModel()`. A\n`LiveModelReplicaCache` follows a live model client handle and yields a `ReplicaInstance`:\n\n```ts\nimport { createImmutableMobxStore } from '@emdash/wire/mobx';\n\nconst conversations = createLiveModelReplicaCache(api.conversation, contractClient.conversation, {\n  lingerMs: 30_000,\n  stores: {\n    state: () => createImmutableMobxStore(),\n  },\n  onChange: {\n    state: (value, meta) => console.log(value, meta.kind),\n  },\n});\n\nconst lease = conversations.acquire({ conversationId: 'demo' });\nconst conversation = await lease.ready();\n\nconst updated = await conversation.mutations.setTitle({ title: 'Replicated' });\nawait updated.settled;\nconsole.log(conversation.states.state.current());\n```\n\n`ReplicaInstance.states` contains one `ReplicaState` per contract member.\n`ReplicaState` follows upstream snapshots and updates, stores current state in a\npluggable `StateStore`, and re-emits updates in a replica-local cursor space. If\nno store is supplied, the replica uses the plain immutable store.\nMobX consumers can pass stores per liveState through `stores`. Use\n`createImmutableMobxStore()` for snapshot/reference consumers such as\n`useSyncExternalStore`, or `createReactiveMobxStore()` for `observer` components\nthat benefit from property-level MobX tracking.\n\nMutation helpers return `{ result, settled }`. On success, the replica translates\nupstream cursors to local cursors before returning them to downstream clients, so\nboth local UI and served clients settle against the same local state.\n\nA cursor-translation timeout never fails a committed mutation. If the replica\ncannot observe the mutation's upstream cursor in time, the mutation still\nresolves successfully and the result data carries `settled: false` (client-side\nmetadata; the wire shape of `LiveMutationResult` is unchanged). Instrumentation\nrecords the timeout via `cursorTranslationTimeout`, and the `settled` promise\nstays best-effort as before.\n\n## Log Replicas\n\nUse a `LiveLogReplicaCache` when a process needs a local retained text buffer or wants\nto serve log output downstream:\n\n```ts\nimport { createMobxLogStore } from '@emdash/wire/mobx';\n\nconst outputs = createLiveLogReplicaCache(api.ptyOutput, contractClient.ptyOutput, {\n  lingerMs: 10_000,\n  maxBufferBytes: 1024 * 1024,\n  store: () => createMobxLogStore(),\n});\n\nconst lease = outputs.acquire({ sessionId });\nconst output = await lease.ready();\noutput.onAppend((chunk) => index(chunk));\nconsole.log(output.text());\n```\n\nWithout a custom sink, the log replica keeps an eager internal `LiveLogSource` buffer\nthat is readable through `text()` and can serve downstream clients. With a custom\nsink, the sink owns storage: readable `LogStore`s expose `text()`, while\nwrite-only `LogSink`s (for example xterm scrollback) support reset/append without\nallocating a duplicate string buffer.\n\nFor terminal rendering, prefer the client handle directly:\n\n```ts\nconst output = contractClient.ptyOutput.handle({ sessionId });\nterm.write((await output.snapshot()).data.text);\nconst detach = await output.attach((update) => {\n  term.write((update.delta as { chunk: string }).chunk);\n});\n```\n\n## Job Replicas\n\n`LiveJobReplicaCache` wraps a live job client handle. It forwards `start()` and `cancel()`,\nmaterializes job state by `jobId`, and keeps terminal state readable under lease\nor retention:\n\n```ts\nimport { createImmutableMobxStore } from '@emdash/wire/mobx';\n\nconst jobs = createLiveJobReplicaCache(api.build, contractClient.build, {\n  lingerMs: 30_000,\n  store: () => createImmutableMobxStore(),\n});\n\nconst lease = await jobs.start({ target: 'desktop' });\nconst job = await lease.ready();\njob.onProgress((progress) => console.log(progress.step));\nconsole.log(await job.result);\n\nawait lease.release();\n\nconst late = jobs.acquire(job.jobId);\nconsole.log((await late.ready()).getState());\n```\n\nWhen a job reaches `succeeded`, `failed`, or `cancelled`, the replica detaches\nfrom the upstream live topic but retains the local terminal state while leases or\n`lingerMs` keep it warm.\nThe job replica also supports a pluggable reset/current `JobStore`, so\n`getState()` can be observable. The internal `LiveStateSource` used for downstream\nserving is created only if `snapshot()` or `subscribe()` is requested.\n\n## Serving Replicas\n\nReplicas can be passed directly into `createController()`:\n\n```ts\nconst upstream = client(api, connect(sshTransport));\n\nconst controller = createController(api, {\n  conversation: createLiveModelReplicaCache(api.conversation, upstream.conversation),\n  ptyOutput: upstream.ptyOutput, // forward bytes without buffering\n  build: createLiveJobReplicaCache(api.build, upstream.build),\n});\n```\n\nUse replicas only when the hop needs local state. If the hop is a pure relay,\nforward the client subtree instead.\n\n<!-- File: packages/wire/docs/observability.md (Tokens: ~672 | Category: anthropic-skill) -->\n# Observability\n\n`@emdash/wire` exposes observability at the same boundaries that own runtime\nbehavior: server calls, client calls, live topic attachment, mutation dedupe,\nlive-client resyncs, transport messages, and scope cleanup.\n\n## Ambient Logger\n\nThe logger interface and redaction utilities live in `@emdash/shared/logger`.\nWire uses the shared ambient logger to attach call context without passing a\nlogger through every domain function:\n\n```ts\nimport { log, runWithLogger } from '@emdash/shared/logger';\n\nawait runWithLogger(logger.child({ requestId: 'r1' }), async () => {\n  log.info('handling request');\n});\n```\n\nNode entry points can install the `AsyncLocalStorage` store:\n\n```ts\nimport { installAsyncLogContext } from '@emdash/shared/logger/node';\n\ninstallAsyncLogContext();\n```\n\nBrowser and renderer code use a synchronous fallback. It still supports a root\nlogger and scoped synchronous blocks, but it does not preserve context across\n`await`.\n\n## Instrumentation Hooks\n\nUse `WireInstrumentation` (exported from `@emdash/wire/rpc`) for typed events\nthat can be adapted to logs, metrics, or tracing. The seam is threaded through\nthe public options of `serve()`, `connect()`, replicas, and `expose()`:\n\n```ts\nimport type { WireInstrumentation } from '@emdash/wire/rpc';\n\nconst instrumentation: WireInstrumentation = {\n  callEnd: (event) => logger.debug('call finished', event),\n  resyncFailed: (event) => logger.warn('resync failed', event),\n};\n\nserve(transport, controller, { logger, instrumentation });\nconst connection = connect(clientTransport, { instrumentation });\nconst contractClient = client(api, connection);\n```\n\nHooks currently cover:\n\n- procedure start/end, cancellation, and duration.\n- snapshot timing and errors.\n- live topic attach/detach.\n- live model/log resync reasons and resync failures.\n- replica cursor-translation timeouts.\n- mutation dedupe hits.\n- scope cleanup errors.\n- transport connect/disconnect events.\n\nThere are no bundled logger adapters: adapt the typed events to your logging or\nmetrics stack at the application edge.\n\nServing and client options are documented in [API serving](./api/serving.md).\nTransport construction is documented in [API transports](./api/transports.md).\n\n## Scope Logging\n\n`createScope({ logger, label })` attaches a logger to the scope tree. Children\ninherit the logger with a `scope` binding containing their label path:\n\n```ts\nconst runtimeScope = createScope({ label: 'runtime', logger });\nconst sessionScope = runtimeScope.child('session:abc');\n\nsessionScope.log.info('session started');\n```\n\nCleanup errors and failed runs are reported through the scope logger by default.\nUse `describeScope(scope)` for a lightweight label tree when debugging retained\nresources or stuck async work. Scope descriptions include active run labels,\nstart times, and cancellation state. When the scope is created with a custom\n`Clock`, those start times come from that clock, which keeps lifecycle diagnostics\ndeterministic in tests.\n\nScope lifecycle behavior is documented in [runtime lifecycle](./runtime/lifecycle.md)\nand [structured concurrency](./runtime/structured-concurrency.md).\n\n<!-- File: packages/wire/docs/runtime/components.md (Tokens: ~1905 | Category: anthropic-skill) -->\n# Components\n\n`WireComponent` is the authored abstraction for runtime and service contracts that can be created\nin-process or served from a worker process.\n\nUse a component when a contract has a reusable runtime implementation and the composition root should\nchoose whether it runs in the current process or in a supervised worker. A component is not a\ndependency injection container: it describes one contract implementation, its explicit requirements,\nits config schema, and how to synchronously create a scope-owned instance.\n\n## Define A Component\n\n```ts\nimport { defineContract, procedure, createController } from '@emdash/wire/rpc';\nimport { defineWireComponent, requireContract } from '@emdash/wire/worker';\nimport { z } from 'zod';\n\nexport const clockContract = defineContract({\n  now: procedure({ input: z.void().optional(), output: z.number() }),\n});\n\nexport const counterContract = defineContract({\n  increment: procedure({ input: z.void().optional(), output: z.number() }),\n});\n\nconst component = defineWireComponent({\n  id: 'counter',\n  contract: counterContract,\n  requirements: {\n    clock: requireContract(clockContract),\n  },\n  configSchema: z.object({\n    initialValue: z.number().int().default(0),\n  }),\n  create: ({ scope, dependencies, config, instance }) => {\n    let value = config.initialValue;\n    scope.add(() => {\n      value = 0;\n    });\n\n    return instance({\n      scope,\n      controller: createController(counterContract, {\n        increment: async () => {\n          await dependencies.clock.now(undefined);\n          value += 1;\n          return value;\n        },\n      }),\n    });\n  },\n});\n```\n\n`create` is synchronous. Expected async work belongs in domain procedures, `LiveJobSource`s, machines,\nlive state, or other explicit runtime APIs. Directory creation, installs, probing, and startup\noperations should expose their own lifecycle and errors rather than hiding inside component\nconstruction.\n\n`instance({ scope, controller })` adapts the controller into a typed in-process client using a memory\ntransport. The controller should be unvalidated; the component creation boundary applies validation\naccording to the caller's `validate` policy.\n\n## Explicit Requirements\n\nRequirements are plain object keys. A component can require another Wire contract with\n`requireContract(contract)`. Deployment values such as paths, limits, executable names, and feature\nflags belong in typed component config, not dependency requirements.\n\nThere is no container, provider registry, singleton policy, recursive construction, or automatic\nresolution. Composition roots create or spawn the dependencies they want and pass dependency clients\nexplicitly. Worker hosts may also accept dependency controllers and forward them over dependency\nchannels.\n\n```ts\nconst clock = defineWireComponent({\n  id: 'clock',\n  contract: clockContract,\n  requirements: {},\n  configSchema: z.object({}),\n  create: ({ instance, scope }) =>\n    instance({\n      scope,\n      controller: createController(clockContract, {\n        now: () => Date.now(),\n      }),\n    }),\n});\n```\n\n## In-Process Creation\n\n`component.create({ scope, dependencies, config, validate })` validates config and dependency keys,\ncreates a child scope, wires a memory transport, serves the controller, and returns a typed client.\nDisposing the instance stops the in-memory server and disposes the component scope.\n\nController factories used by components should return unvalidated controllers. The component\ncreation or worker serving boundary applies validation once.\n\n```ts\nimport { createScope } from '@emdash/shared/concurrency';\n\nconst scope = createScope({ label: 'app' });\nconst clockInstance = clock.create({\n  scope,\n  dependencies: {},\n  config: {},\n  validate: 'inputs',\n});\n\nconst counterInstance = component.create({\n  scope,\n  dependencies: {\n    clock: clockInstance.client,\n  },\n  config: {\n    initialValue: 41,\n  },\n  validate: 'inputs',\n});\n\nawait counterInstance.client.increment(undefined); // 42\nawait scope.dispose();\n```\n\nThe caller owns the parent scope. Each component creates a child scope under it. Disposing either the\ninstance or the parent scope releases the component resources.\n\n## Worker Deployment\n\n`WireWorkerHost.create(component, options)` returns a lazy supervised worker. `worker.ready()`\nstarts it and returns the stable typed client. `spawn(component, options)` eagerly starts the\nworker and returns that client.\n\nWorker component IPC uses internal framed logical channels:\n\n- `runtime` for the served component contract.\n- bootstrap messages for config and readiness.\n- one `dep:<name>` channel per contract requirement.\n\nDependency channels are forwarded only from the explicit `dependencies` object supplied by the\ncomposition root. Child workers receive typed clients for those channels; they do not locate or\nstart dependencies themselves.\n\nParent process:\n\n```ts\nimport { createScope } from '@emdash/shared/concurrency';\nimport { createWireWorkerHost } from '@emdash/wire/worker';\nimport { childProcessSpawner } from '@emdash/wire/worker/node';\nimport { clock, component as counterComponent } from './component';\nimport { workerPath } from './worker-manifest';\n\nconst scope = createScope({ label: 'app' });\nconst host = createWireWorkerHost({\n  scope,\n  processSpawner: childProcessSpawner(),\n});\n\nconst clockInstance = clock.create({\n  scope,\n  dependencies: {},\n  config: {},\n});\n\nconst counterWorker = host.create(counterComponent, {\n  executable: workerPath('counter'),\n  dependencies: {\n    clock: clockInstance.client,\n  },\n  config: {\n    initialValue: 0,\n  },\n});\n\nconst counterClient = await counterWorker.ready();\nawait counterClient.increment(undefined);\n```\n\nWorker entry:\n\n```ts\nimport { runWireComponentWorker } from '@emdash/wire/worker';\nimport { component as counterComponent } from './component';\n\nvoid runWireComponentWorker(counterComponent);\n```\n\nThe client returned by `ready()` is stable across process generations. Calls fail while the worker is\nunavailable; the client does not buffer calls during restarts.\n\n## Unrecoverable Failures\n\nComponent code can call `fatal(error)` when process-local state is poisoned and the current instance\ncannot safely serve later requests. Fatal failures retire the component scope exactly once. An\nin-process caller can observe the failure through `onFatal`; `runWireComponentWorker` exits with code\n`1`, allowing the owning `WireWorkerHost` to replace that process generation under its supervision\npolicy.\n\nUse `fatal` only when replacing the instance is the recovery mechanism, such as a native operation\nthat remains pending after its deadline and cannot be cancelled in-process. Expected request errors\nshould still be returned or thrown through the contract. A fatal report does not make an unresponsive\nnative call safe to overlap with another call in the same process; worker deployment recovers by\nterminating the process that owns the native state.\n\n## Testing Components\n\nFor in-process tests, create the component under a test scope and pass fake dependency clients\nexplicitly.\n\n```ts\nimport { createScope } from '@emdash/shared/concurrency';\nimport { createController } from '@emdash/wire/rpc';\nimport { defineWireComponent } from '@emdash/wire/worker';\nimport { z } from 'zod';\n\nconst scope = createScope({ label: 'test' });\nconst fakeClockComponent = defineWireComponent({\n  id: 'fake-clock',\n  contract: clockContract,\n  requirements: {},\n  configSchema: z.object({}),\n  create: ({ instance, scope }) =>\n    instance({\n      scope,\n      controller: createController(clockContract, {\n        now: () => 1,\n      }),\n    }),\n});\nconst fakeClock = fakeClockComponent.create({\n  scope,\n  dependencies: {},\n  config: {},\n});\n\nconst counter = component.create({\n  scope,\n  dependencies: {\n    clock: fakeClock.client,\n  },\n  config: {\n    initialValue: 0,\n  },\n  validate: 'full',\n});\n\nawait expect(counter.client.increment(undefined)).resolves.toBe(1);\nawait scope.dispose();\n```\n\nFor worker tests, use `FakeWorkerProcessSpawner` and run the child side with\n`runWireComponentWorker(component, { port: fakeProcess.childPort })`. This exercises bootstrap,\nlogical channel isolation, dependency forwarding, readiness, and reconnect behavior without starting\na real child process.\n\n## Build Metadata\n\nManual worker manifests in the desktop app and workspace-server are temporary build metadata. They\nmap thin entry files to emitted worker artifacts until a bundler plugin owns worker artifact\ngeneration. They are not part of the component runtime API.\n\n## Guidelines\n\n- Keep component factories synchronous and cheap.\n- Report poisoned process-local state through `fatal`; keep expected operation failures on the\n  contract.\n- Pass dependencies by name from the composition root; never look them up from a global registry.\n- Prefer contract requirements for shared capabilities that may cross process boundaries.\n- Use typed config for deployment-specific values such as paths, limits, executable names, and\n  feature flags.\n- Keep controller validation at the component creation or worker serving boundary, not inside every\n  controller factory.\n- Keep manual worker manifests and entry files thin; they should only select a component and call\n  `runWireComponentWorker(component)`.\n\n<!-- File: packages/wire/docs/runtime/lifecycle.md (Tokens: ~2525 | Category: anthropic-skill) -->\n# Lifecycle Utilities\n\n`Scope`, `LifecycleRegistry`, and the resource cache primitives are\ndependency-light Shared lifecycle utilities exported from\n`@emdash/shared/concurrency`. Scheduling utilities are exported separately from\n`@emdash/shared/scheduling`. Wire uses these primitives, but they do not define\nWire protocol messages.\n\n- `Scope` owns cleanup and async work for a tree of resources.\n- `LifecycleRegistry` owns explicit keyed start/stop state machines.\n- `Clock` owns sleeps, deadlines, retry backoff, and disposable timers.\n- `ResourceCache` turns keyed demand into retained resources with ref-counted leases.\n- `SharedResource` is the unkeyed form for one lazily created resource.\n- `AsyncCache` caches async values that do not have finalizers.\n- `Mailbox` is a bounded async handoff queue for one logical consumer.\n\nThey do not define wire messages and can run in browser, renderer, main process,\nor tests.\n\n## Scope\n\nUse a scope when a feature creates more than one disposable resource and those\nresources should die together:\n\n```ts\nimport { createScope } from '@emdash/shared/concurrency';\n\nconst scope = createScope({ label: 'conversation:abc', logger });\n\nscope.add(() => detachLiveState());\nscope.add(() => removeWindowListener());\nscope.use({ dispose: () => runtime.dispose() });\nscope.run('refresh', async (signal) => {\n  await clock.sleep(250, { signal });\n  await refreshVisibleState({ signal });\n});\n\nawait scope.dispose();\n```\n\nSemantics:\n\n- `state` is `open`, `closing`, or `closed`.\n- `signal` aborts synchronously when disposal begins.\n- `run(label, fn)` starts a tracked async operation with its own abort signal.\n- Cleanups run in reverse registration order.\n- Child scopes and active runs settle before the parent's own cleanups.\n- Cleanup errors are reported through `onCleanupError` and do not stop later\n  cleanups.\n- `dispose()` is idempotent.\n- `add()` on an already disposed scope runs the cleanup immediately.\n- `Run.exit` never rejects; use `Run.value()` when ordinary promise rejection is\n  desired.\n\nThat last rule makes async attach/dispose races easier to handle:\n\n```ts\nconst scope = createScope();\nawait scope.dispose();\n\nscope.add(() => detachTopic()); // runs immediately\n```\n\n## Scope Logging\n\nEvery scope has `scope.log`. If a logger is supplied at the root, children\ninherit it with a `scope` binding for the accumulated label path:\n\n```ts\nconst runtimeScope = createScope({ label: 'runtime', logger });\nconst sessionScope = runtimeScope.child('session:one');\n\nsessionScope.log.info('session attached');\n```\n\nDefault cleanup error handling logs through the scope logger:\n\n```ts\nconst scope = createScope({ label: 'view', logger });\nscope.add(() => {\n  throw new Error('cleanup failed');\n});\n\nawait scope.dispose(); // logger.warn('scope cleanup failed', ...)\n```\n\nCustom cleanup handlers receive `{ label, labelPath, logger }`:\n\n```ts\nconst scope = createScope({\n  label: 'root',\n  onCleanupError: (error, context) => {\n    context.logger.warn('custom cleanup failure', {\n      labelPath: context.labelPath,\n      error,\n    });\n  },\n});\n```\n\nUse `describeScope(scope)` to inspect the live label tree:\n\n```ts\nconsole.log(describeScope(runtimeScope));\n```\n\nThe description contains labels, label paths, lifecycle state, active runs, and\nchild descriptions. It does not expose cleanup callbacks.\n\nWhen `createScope({ clock })` is used, child scopes inherit the same clock and\nrun diagnostics use deterministic timestamps.\n\n## Scope Runs\n\nUse `run()` when async work must not outlive the scope:\n\n```ts\nconst run = sessionScope.run('attach-transcript', async (signal) => {\n  const detach = await transcript.attach(updateView, { signal });\n  sessionScope.add(detach);\n});\n\nawait run.exit;\n```\n\nDisposing `sessionScope` cancels the run and waits for it before running the\nscope's cleanups. Cancellation is cooperative: pass the provided signal to any\noperation that can wait, sleep, perform I/O, or subscribe.\n\nUse `run()` for background jobs, process transitions, replica attachment,\nrenderer bindings, and async setup. Use `add()` or `use()` for finalizers. Use\n`ResourceCache` when a keyed resource should be shared by leases.\n\nFor the full lifecycle model and invariants, see\n[Structured concurrency](./structured-concurrency.md).\n\n## LifecycleRegistry\n\nUse `LifecycleRegistry` when a feature owns a keyed set of resources that are\nstarted and stopped by explicit commands and callers need observable lifecycle\nstate:\n\n```ts\nimport { createLifecycleRegistry } from '@emdash/shared/concurrency';\nimport { ok } from '@emdash/shared';\n\nconst runtimes = createLifecycleRegistry<\n  { workspaceId: string },\n  Runtime,\n  StartRuntimeError,\n  { reason: string },\n  StopRuntimeError\n>({\n  label: 'workspace-runtimes',\n  scope: appScope,\n  keyOf: (input) => input.workspaceId,\n  start: async (input, scope, signal) => {\n    const runtime = await createRuntime(input.workspaceId, { signal });\n    scope.add(() => runtime.dispose());\n    return ok(runtime);\n  },\n  stop: async (_key, runtime, context, _scope, signal) => {\n    await runtime.stop({ reason: context?.reason, signal });\n    return ok(undefined);\n  },\n  onStateChanged: ({ key, current }) => {\n    logger.info('runtime state changed', { key, state: current.kind });\n  },\n});\n```\n\nSelection guidance:\n\n- Use `Scope` for ownership, cancellation, cleanup ordering, and async work that\n  must not outlive its feature. It is not a keyed start/stop registry.\n- Use `LifecycleRegistry` for local keyed resources with explicit `start()`,\n  `stop()`, `register()`, `state()`, and state-change observation.\n- Use `ResourceCache` when demand is lease-driven and the resource should exist\n  only while consumers hold leases or while an idle TTL retains it.\n- Use `WireWorkerHost.create(component, ...)` when the resource is a Wire worker process with\n  readiness, a stable typed client returned by `ready()`, supervision, restart backoff, and child\n  process generations.\n- Use `LiveJobSource` when callers need a Wire-visible cancellable job with progress,\n  terminal state, retention, and remote client handles.\n\n`LifecycleRegistry` differs from `Scope` by modeling resource state rather than\ngeneral cleanup. It differs from `ResourceCache` by not ref-counting leases or\ncreating resources on demand from `acquire()`. It differs from Wire workers and\n`LiveJobSource` by staying local and protocol-free: it does not supervise processes,\nserve clients, publish progress, or retain terminal job state.\n\n## Mailbox Ownership\n\nUse `Mailbox` when local producers and one consumer need an explicit bounded\nhandoff. Register it with the owning scope, then run the consumer loop under the\nsame scope:\n\n```ts\nimport { createMailbox } from '@emdash/shared/concurrency';\n\nconst mailbox = sessionScope.use(createMailbox<Event>({ capacity: 256 }));\n\nsessionScope.run('drain-events', async () => {\n  for await (const event of mailbox) {\n    await handleEvent(event);\n  }\n});\n```\n\nDisposing `sessionScope` disposes the mailbox and unblocks pending producers or\nconsumers. For state machines, overflow guarantees, and the deferred Broadcast\ncontract, see [Mailbox and Broadcast](./mailbox-and-broadcast.md).\n\n## Child Scopes\n\nUse child scopes to model ownership:\n\n```ts\nconst runtimeScope = createScope({ label: 'runtime' });\nconst sessionScope = runtimeScope.child('session:one');\n\nsessionScope.add(() => stopSession());\nruntimeScope.add(() => stopRuntime());\n\nawait runtimeScope.dispose();\n```\n\nDisposing `runtimeScope` disposes `sessionScope` first, then `stopRuntime()`.\nDisposing `sessionScope` directly deregisters it from the parent so it does not\ndispose twice later.\n\n## ResourceCache\n\n`ResourceCache` is useful when a resource should exist only while somebody is\nusing it: a live topic binding, a renderer view model, a preview server, or a\nprocess-backed session.\n\n```ts\nimport { createResourceCache } from '@emdash/shared/concurrency';\n\nconst sessions = createResourceCache({\n  scope: runtimeScope,\n  label: 'sessions',\n  key: (input: { conversationId: string }) => input.conversationId,\n  idleTtlMs: 30_000,\n  create: async ({ conversationId }, scope) => {\n    const session = await startSession(conversationId);\n    scope.add(() => session.stop());\n    return session;\n  },\n  onError: (error, key) => logger.warn('session creation failed', { key, error }),\n});\n```\n\n`create(key, scope)` is also the disposal hook. Register teardown on the supplied\nscope as soon as resources are acquired. If creation later fails, the scope is\ndisposed and partial resources are cleaned up.\n\nBehavior:\n\n- The first `acquire()` for a key calls `create()`.\n- Concurrent acquires for the same key share the in-flight creation.\n- In-flight creation is tracked as a scope run and is cancelled when the entry is\n  invalidated or the parent scope closes.\n- Each lease increments the refcount.\n- `release()` is idempotent.\n- The last release starts the grace timer.\n- Acquire during the grace timer cancels teardown and reuses the active value.\n- Failed creation is not cached; the next acquire retries.\n- `peek(key)` returns the active value if creation has completed.\n- `dispose()` force-disposes every active or retained entry.\n- Passing `scope` attaches the source and all keyed entries to a parent scope.\n  Disposing that parent scope force-disposes the source and rejects later\n  acquisitions. `label` names the source node in `describeScope()` output.\n- Every input that affects provisioning must be in `K` or fixed in the factory\n  closure. There is no acquire-time creation context.\n\nUsage:\n\n```ts\nconst lease = sessions.acquire({ conversationId: 'abc' });\nconst session = await lease.ready();\n\nawait lease.release();\nawait sessions.dispose();\n```\n\nParent scopes are useful for runtimes with several keyed resources:\n\n```ts\nconst authScope = runtimeScope.child('auth');\nconst logins = createResourceCache({\n  scope: authScope,\n  label: 'login-source',\n  key: (key: { providerId: string; methodId: string; generation: string }) =>\n    `${key.providerId}:${key.methodId}:${key.generation}`,\n  create: async (key, scope) => {\n    const login = await startLoginPty(key.providerId, key.methodId);\n    scope.add(() => login.dispose());\n    return login;\n  },\n});\n\nawait authScope.dispose(); // also disposes every active login entry\n```\n\n## Grace Windows\n\nUse `idleTtlMs` for resources whose demand can flap briefly. For example, a window\nreload may detach and reattach live model bindings within a few hundred\nmilliseconds. A short grace window avoids tearing down the underlying resource\nonly to recreate it immediately.\n\n```ts\nconst bindings = createResourceCache({\n  key: (key: { taskId: string }) => key.taskId,\n  idleTtlMs: 5_000,\n  create: async (key, scope) => {\n    const binding = client.task.transcript(key, (state) => render(state));\n    scope.add(() => binding.dispose());\n    await binding.ready;\n    return binding;\n  },\n});\n```\n\nUse `ResourceCache`, `SharedResource`, or `AsyncCache` for new shared lifetime\nwork. Avoid adding compatibility lifecycle adapters around new code.\n\nSee [../../examples/scope/client.ts](../../examples/scope/client.ts) and\n[../../examples/resource-cache/client.ts](../../examples/resource-cache/client.ts).\n\n<!-- File: packages/wire/docs/runtime/mailbox-and-broadcast.md (Tokens: ~1158 | Category: anthropic-skill) -->\n# Mailbox and Broadcast\n\n`Mailbox<T>` is a Shared bounded asynchronous handoff primitive exported from\n`@emdash/shared/concurrency`. Wire uses it when one logical consumer needs to\ndrain values from one or more producers without hand-writing arrays, waiter\nlists, terminal flags, and overflow policy.\n\n```ts\nimport { createMailbox, createScope } from '@emdash/shared/concurrency';\n\nconst scope = createScope({ label: 'download' });\nconst chunks = scope.use(createMailbox<Uint8Array>({ capacity: 64 }));\n\nconst consumer = scope.run('drain', async () => {\n  for await (const chunk of chunks) {\n    await writeChunk(chunk);\n  }\n});\n\nawait chunks.offer(new Uint8Array([1, 2, 3]));\nchunks.close();\nawait consumer.value();\n```\n\n## Mailbox\n\nA mailbox has many producers and one logical consumer. Values accepted into the\nmailbox are delivered in FIFO order. The consumer can use `take()` for one value\nat a time or `for await` for a draining loop.\n\n```ts\nconst mailbox = createMailbox<Job>({\n  capacity: 128,\n  overflow: 'suspend',\n});\n\nawait mailbox.offer(job, { signal });\nconst next = await mailbox.take({ signal });\n```\n\n### State Machine\n\n```mermaid\nstateDiagram-v2\n  [*] --> Open\n  Open --> Open: offer / take\n  Open --> Closing: close()\n  Closing --> Closed: buffer drained\n  Open --> Failed: fail(error)\n  Failed --> Failed: drain accepted values\n  Failed --> [*]: next take throws error\n  Closed --> [*]\n```\n\n`close()` is graceful: accepted values drain before consumers see the end of the\nstream. `fail(error)` is also ordered: accepted values drain first, then the next\ntake or iterator step throws the failure. `dispose()` is immediate: it clears\nbuffered values and unblocks pending producers and consumers.\n\n### Guarantees\n\n- Accepted values are delivered in FIFO order.\n- A value is delivered to at most one consumer.\n- Only one pending `take()` or active async iterator is allowed at a time.\n- Capacity is explicit and always finite.\n- Overflow is explicit: `suspend`, `reject`, `drop-oldest`, or `drop-newest`.\n- `tryOffer()` never waits.\n- `offer()` waits only when overflow is `suspend`.\n- `offer()` and `take()` can observe an `AbortSignal`; aborting one operation does\n  not close the mailbox.\n- Terminal operations are idempotent.\n- `scope.use(mailbox)` ties disposal to the owning scope, but consumer loops should\n  still run under `scope.run()` when they must not outlive the feature.\n\n### Overflow Policies\n\nUse `suspend` when every accepted value matters and producers can wait. Use\n`reject` when overflow is a protocol or invariant violation. Use `drop-oldest` for\nbest-effort queues where freshness matters more than history. Use `drop-newest`\nwhen existing queued work should be preserved and incoming work can be ignored.\n\nBlob channels use `reject` internally because dropping bytes would corrupt the\nstream. The reconnecting transport uses a shared bounded buffer with\n`drop-oldest`, preserving the existing policy that old non-blob frames are less\nvaluable than recent ones while disconnected.\n\n### Non-Goals\n\nMailbox is not:\n\n- a durable queue;\n- a multi-consumer work queue;\n- a replay log;\n- an acknowledgement or retry protocol;\n- a mutex;\n- a domain error channel.\n\nExpected domain failures should still be represented as `Result<T, E>`. Mailbox\nfailure means the local handoff itself was terminated by lifecycle, protocol, or\ntransport failure.\n\n## Broadcast Contract\n\n`Broadcast<T>` is the reserved name for a future local asynchronous fan-out\nprimitive. It should not be called `Topic<T>` because Wire already uses “topic” for\nremote live subscription addresses.\n\nThe intended shape is one mailbox per subscriber:\n\n```mermaid\nflowchart LR\n  publisher[Publisher] --> broadcast[Broadcast]\n  broadcast --> subA[\"Subscriber A Mailbox\"]\n  broadcast --> subB[\"Subscriber B Mailbox\"]\n  broadcast --> subC[\"Subscriber C Mailbox\"]\n```\n\nThe intended guarantees are:\n\n- `publish(value)` never runs subscriber application code directly.\n- Each subscriber receives values through its own mailbox.\n- Slow subscribers apply only their own overflow policy.\n- Per-subscriber delivery is FIFO for accepted values.\n- New subscribers do not receive replay.\n- Closing or failing the broadcast propagates to every subscription.\n- Publish results report subscriber count, delivered count, and dropped count.\n\nDo not implement or migrate to `Broadcast<T>` until a source has at least two\ngenuinely asynchronous consumers that need independent buffering or overflow\npolicies. Current Wire sources do not meet that bar: process stdio has one\nproduction subscriber, and observability hooks already isolate synchronous\nfailures.\n\n## Where Not To Use These\n\nDo not replace `LiveStateSource`, `LiveLogSource`, `EventStreamSource`, replicas, live\nfollowers, or reconnect attachment logic with Mailbox or Broadcast. Those\nprimitives carry generation, sequence, snapshot, resync, reconnect, retention, or\ngap semantics that Mailbox deliberately does not provide.\n\nDo not replace condition waiters such as live cursor waiters or renderer version\nwaiters. They wait for a predicate over monotonic state; they do not queue values.\n\nSee also [Structured concurrency](./structured-concurrency.md) and\n[Lifecycle utilities](./lifecycle.md).\n\n<!-- File: packages/wire/docs/runtime/resource-cache.md (Tokens: ~691 | Category: anthropic-skill) -->\n# Resource Caches\n\n`ResourceCache`, `SharedResource`, and `AsyncCache` are Shared primitives from\n`@emdash/shared/concurrency`. Wire uses them for shared lifetimes and cached\nasync values, but they do not define Wire protocol messages.\n\n## Choose A Primitive\n\n- Use `ResourceCache<K, T>` for keyed resources that need leases and teardown:\n  process sessions, replicas, file watchers, tree resources, and connection pools.\n- Use `SharedResource<T>` for one lazily created resource with the same lease\n  semantics: a lazy worker, singleton client, or shared runtime helper.\n- Use `AsyncCache<K, T>` for data without finalizers: auth status probes,\n  metadata lookups, or other retryable reads.\n- Use `deduplicate()` middleware only when you want in-flight sharing without\n  retaining successful values.\n\n## Resource Identity\n\nEvery input that can change provisioning must be part of `K` or captured by the\nfactory closure. The cache only receives `acquire(key)`, so there is no separate\ncreation context:\n\n```ts\nconst logins = createResourceCache({\n  key: (key: { providerId: string; methodId: string; generation: string }) =>\n    `${key.providerId}:${key.methodId}:${key.generation}`,\n  create: async (key, scope) => {\n    const pty = await startLoginPty(key.providerId, key.methodId);\n    scope.add(() => pty.dispose());\n    return pty;\n  },\n});\n```\n\n## Scope Ownership\n\nEach cache owns an internal scope. Each entry gets a child scope, and creation runs\nunder `scope.run('create', ...)`. Register teardown on the entry scope as soon as\nresources are acquired. If creation fails, the entry scope is disposed and the\nnext acquire retries.\n\n`invalidate(key)` and `dispose()` force-close entries even if leases are still\nheld. Releasing a stale lease later is a no-op. This keeps owner shutdown from\nwaiting forever on leaked consumers.\n\n## Lease Behavior\n\n- Concurrent `acquire(key)` calls share one in-flight creation.\n- Every acquire receives its own idempotent lease.\n- `peek(key)` returns only completed active values.\n- Failed creation is not cached.\n- By default, releasing the final lease during creation retains the in-flight entry so a later\n  acquire can share it and the factory's terminal result remains authoritative.\n- Set `cancelPendingOnRelease: true` for resources such as watchers that have no useful lifetime\n  without a lease. Pending work must then observe the entry scope's signal and clean up late results.\n- `idleTtlMs` retains a zero-ref entry briefly so flapping demand can reuse it.\n- Acquiring during teardown waits for teardown to finish, then provisions fresh.\n\n## AsyncCache\n\n`AsyncCache` shares concurrent loads and caches only successes:\n\n```ts\nconst statuses = createAsyncCache({\n  key: (providerId: string) => providerId,\n  ttlMs: 15 * 60 * 1000,\n  load: (providerId, signal) => checkAuthStatus(providerId, { signal }),\n});\n```\n\n`get()` uses a cached value when fresh, `refresh()` cancels the current load and\nstarts a new generation, `set()` publishes an explicit value, and failures are\nnever retained.\n\n<!-- File: packages/wire/docs/runtime/scheduling.md (Tokens: ~983 | Category: anthropic-skill) -->\n# Scheduling\n\n`@emdash/shared/scheduling` centralizes time, retry, and\ncancellation-sensitive waits. Wire uses these Shared primitives, and runtime\ncode should depend on `Clock` instead of calling platform timer APIs directly.\n\n```ts\nimport { retry, retrySchedules, systemClock } from '@emdash/shared/scheduling';\n\nawait retry(\n  () => transport.connect(),\n  {\n    clock: systemClock,\n    schedule: retrySchedules.exponential({ initialMs: 100, maxMs: 2_000 }),\n    shouldRetry: (error) => isTransient(error),\n  }\n);\n```\n\n## Clock\n\n`Clock` has three operations:\n\n- `now()` returns wall-clock milliseconds for diagnostics and deadlines.\n- `schedule(delayMs, callback)` returns a disposable `TimerHandle`.\n- `sleep(delayMs, { signal })` returns an abortable promise.\n\nUse `Clock` for sleeps, backoff, debounce, retention, idle TTLs, readiness\ndeadlines, and test polling. Keep direct `Date.now()` or `performance.now()` for\nprotocol timestamps, generated IDs, and duration measurement where the wall clock\nor monotonic timer is the data being recorded.\n\n## TimerHandle Ownership\n\nEvery scheduled callback must have one owner. Register the handle with a `Scope`,\nstore it on the owning entry, or dispose it in all terminal paths:\n\n```ts\nconst handle = clock.schedule(5_000, () => expire(id), { unref: true });\nscope.add(() => handle.dispose());\n```\n\n`TimerHandle.dispose()` is idempotent. The `active` flag becomes false before the\ncallback runs, so callback and disposal races remain at-most-once.\n\n## Timeouts\n\n`runWithTimeout(work, { timeoutMs, signal, clock })` derives a child\n`AbortSignal`, schedules a deadline on the provided `Clock`, and disposes its\ntimer in every terminal path. The returned promise rejects with `TimeoutError`\nwhen the deadline expires, but parent cancellation still rejects with the\nparent's abort reason.\n\n```ts\nconst result = await runWithTimeout(\n  (signal) => loadWorkspace({ signal }),\n  { timeoutMs: 5_000, signal: parentSignal, clock }\n);\n```\n\nThe timeout rejects promptly even when work ignores the derived signal, but the\nunderlying JavaScript work can only stop cooperatively. Pass the derived signal\ninto sleeps, process waits, transports, and any helper that can observe\ncancellation.\n\n## ManualClock\n\nTests should import `ManualClock` from `@emdash/shared/testing`. It preserves FIFO\nordering for timers with the same deadline and flushes resumed promise\nmicrotasks while advancing:\n\n```ts\nconst clock = new ManualClock(1_000);\nconst done = clock.sleep(250);\n\nawait clock.advanceBy(250);\nawait done;\n```\n\nUse `runAll({ maxTimers })` only when the test expects the scheduled work to\ndrain. The guard prevents accidental infinite timer loops.\n\n## Retry Semantics\n\n`RetrySchedule.delayFor(0)` is the delay before the first retry. Attempt `0` is\nthe initial operation call, attempt `1` is the first retry, and so on.\n`undefined` means retries are exhausted.\n\nSchedules are stateless and composable: fixed, finite sequence, repeat-last\nsequence, exponential, limited, and deterministic jitter. Inject a deterministic\nrandom function into jittered schedules in tests.\n\n`retry()` is exception-based. Domain errors that are expected API results should\nremain `Result<T, E>` values; use `shouldRetry` to classify thrown transport or\nruntime failures.\n\n## Wire Retry Vocabulary\n\nWire expresses all retry timing through one schedule vocabulary:\n`RetrySchedule` and the `retrySchedule()` constructor from\n`@emdash/shared/scheduling`. The reconnecting transport's reconnect delays,\nworker-slot supervision, the follower's `resyncRetry` policy, and per-call\nopt-in retries all consume it. Shared vocabulary, not shared state — each site\nkeeps its own independently tunable schedule:\n\n```ts\nimport { retrySchedule } from '@emdash/shared/scheduling';\n\nconst supervision = {\n  restart: 'on-failure',\n  schedule: retrySchedule({ delaysMs: [250, 1_000, 2_500], repeatLast: true, maxRetries: 5 }),\n};\n```\n\n## Periodic Work\n\nPrefer abortable loops or chained one-shot timers over overlapping intervals.\nThat keeps disposal ordered and avoids installing work after an owner has begun\nclosing:\n\n```ts\nscope.run('poll', async (signal) => {\n  while (!signal.aborted) {\n    await refresh({ signal });\n    await clock.sleep(1_000, { signal });\n  }\n});\n```\n\n<!-- File: packages/wire/docs/runtime/structured-concurrency.md (Tokens: ~1119 | Category: anthropic-skill) -->\n# Structured Concurrency\n\nShared `Scope.run()` from `@emdash/shared/concurrency` tracks asynchronous work\nunder the same owner as subscriptions, processes, timers, and other resources.\nWire uses it when work must not outlive the feature that started it.\n\n```ts\nconst scope = createScope({ label: 'session:abc', logger, clock });\n\nconst run = scope.run('load-history', async (signal) => {\n  const history = await loadHistory({ signal });\n  return history;\n});\n\nconst history = await run.value();\nawait scope.dispose();\n```\n\n## Concepts\n\n- `Scope` owns a lifecycle tree. Parent disposal closes children.\n- `Run<A>` is one async operation owned by a scope.\n- `Run.exit` never rejects. It resolves to `success`, `failure`, or `cancelled`.\n- `Run.value()` converts the exit back to normal promise semantics.\n- `Lease<A>` is temporary use of a shared resource, usually from `ResourceCache`\n  or `SharedResource`.\n\n```mermaid\nflowchart TD\n  featureScope[\"Feature Scope\"] --> childScope[\"Child Scope\"]\n  featureScope --> runA[\"Run: load-history\"]\n  childScope --> runB[\"Run: attach-topic\"]\n  childScope --> cleanup[\"Finalizers\"]\n```\n\n## Lifecycle\n\nDisposal is idempotent and ordered:\n\n1. The scope moves from `open` to `closing`.\n2. `scope.signal` aborts synchronously.\n3. Existing child scopes begin closing.\n4. Active runs are cancelled.\n5. The scope waits for child scopes and runs.\n6. Cleanups run in reverse registration order.\n7. Cleanup errors are logged and later cleanups still run.\n8. The scope moves to `closed`.\n\nStarting a run after closing has begun does not invoke the operation. The returned\nrun is already cancelled. Adding a cleanup after a scope is closed runs it\nimmediately as best-effort cleanup.\n\n## Invariants\n\n- Every child scope has one parent.\n- Every run belongs to one scope.\n- `dispose()` returns the same promise for every caller.\n- `scope.signal` aborts before the first `await` in disposal.\n- Parent cancellation propagates to children.\n- Sibling runs do not cancel each other.\n- `Run.exit` never rejects.\n- A cancelled run is reported as `cancelled` even if the operation later returns.\n- A failed run is logged through the scope logger.\n- A scope is not closed until owned runs and children have settled.\n\nJavaScript cancellation is cooperative. A run that ignores its signal can keep\n`dispose()` pending. Pass the run signal into network calls, sleeps, process\nwaits, and any helper that can observe cancellation.\n\n`Scope` accepts an optional `Clock`. Child scopes inherit it, and active-run\ndiagnostics use `clock.now()` for deterministic timestamps in tests. Prefer\n`clock.sleep(ms, { signal })` for run-owned delays so disposal can interrupt the\nwait immediately.\n\nA run must not await disposal of its own owning scope. That creates a cycle:\n`dispose()` waits for the run, while the run waits for `dispose()`.\n\n## When To Use It\n\nUse `run()` for:\n\n- background jobs and progress sources;\n- pending replica attachment or snapshot work;\n- process spawn, restart backoff, and exit handling;\n- renderer bindings with fire-and-forget RPCs;\n- async setup that must be cleaned up if later setup fails.\n\nUse `add()` or `use()` for:\n\n- subscriptions;\n- timers and observers;\n- resource finalizers;\n- objects with `dispose()`.\n\nUse child scopes for:\n\n- per-session resources under a runtime;\n- per-job resources under a job source;\n- per-consumer resources under a shared cache;\n- modal or view-local lifetimes.\n\nUse `ResourceCache` for keyed, ref-counted resources. Its creation runs under\nthe entry scope, so invalidating an entry or disposing the parent scope cancels\nand joins in-flight creation before finalizers run.\n\nUse `Mailbox` for bounded asynchronous handoff between producers and one logical\nconsumer. The mailbox owns buffering and terminal-state wakeups; run the consumer\nloop under `scope.run()` when it must not outlive the feature:\n\n```ts\nconst mailbox = scope.use(createMailbox<Message>({ capacity: 128 }));\n\nscope.run('drain messages', async () => {\n  for await (const message of mailbox) {\n    await handleMessage(message);\n  }\n});\n```\n\n`Broadcast<T>` is documented as a future local fan-out primitive for multiple\nasynchronous consumers with independent backpressure. Do not use Mailbox or\nBroadcast to replace live-state snapshots, retained logs, event streams, replica\ncursors, or reconnect attachment logic; those protocols carry generation,\nsequence, retention, or resync semantics.\n\nDo not use `run()` as:\n\n- a mutex or serialization primitive;\n- a retry policy;\n- a cache;\n- a domain error channel.\n\nExpected domain failures should still be returned as `Result<T, E>`. A run\nfailure means the operation itself threw unexpectedly.\n\n## Diagnostics\n\n`describeScope(scope)` includes the lifecycle state, active child scopes, and\nactive runs:\n\n```ts\nconsole.log(describeScope(scope));\n```\n\nEach run description includes its label, start time, and whether cancellation has\nstarted. This is useful for finding retained sessions, stuck process transitions,\nor long-running renderer bindings during tests and debugging.\n\n<!-- File: packages/wire/docs/runtime/workers.md (Tokens: ~788 | Category: anthropic-skill) -->\n# Workers\n\nA worker is a named, process-hosted `WireComponent` owned by a `WireWorkerHost`.\nThe host creates logical workers and returns a stable typed client from readiness. Internally, each worker has one\nslot that owns readiness, supervision, reconnects, graceful shutdown, and process generations.\nLow-level spawners create exactly one process generation.\n\nSee [Components](./components.md) for the authored component shape, local creation examples,\ndependency requirements, and testing guidance. This page focuses on process supervision.\n\n## Layers\n\n```mermaid\nflowchart LR\n  App[\"Application composition\"] --> Host[\"WireWorkerHost\"]\n  Host --> Slot[\"Internal worker slot\"]\n  Slot --> Link[\"Internal worker link\"]\n  Slot --> Spawner[\"WorkerProcessSpawner\"]\n  Spawner --> Process[\"WorkerProcess\"]\n  Process --> Runner[\"runWireComponentWorker\"]\n  Runner --> Controller[\"Component Controller\"]\n  Host --> Deps[\"Explicit dependency channels\"]\n```\n\n- `WireWorkerHost` is a process ownership boundary. Composition roots inject a\n  `WorkerProcessSpawner`, logger, clock, and defaults.\n- The internal worker slot is the only multi-generation state machine. It keeps one stable\n  connection/client while child processes restart underneath it; it is not an author-facing API.\n- `WorkerProcessSpawner` is platform-specific and creates one generation. Use\n  `childProcessSpawner()` from `@emdash/wire/worker/node` for Node child\n  processes.\n- `runWireComponentWorker(component)` is the child-side IPC bridge. It bootstraps typed config,\n  connects explicit dependency clients, creates the component, serves its controller, and signals\n  readiness.\n\n## Parent Side\n\n```ts\nimport { createWireWorkerHost } from '@emdash/wire/worker';\nimport { childProcessSpawner } from '@emdash/wire/worker/node';\nimport { createScope } from '@emdash/shared/concurrency';\nimport { counterComponent } from './component';\nimport { workerPath } from './worker-manifest';\n\nconst scope = createScope({ label: 'main' });\nconst host = createWireWorkerHost({\n  scope,\n  processSpawner: childProcessSpawner(),\n});\n\nconst worker = host.create(counterComponent, {\n  name: 'counter',\n  executable: workerPath('counter'),\n  dependencies: {},\n  config: {},\n});\n\nconst client = await worker.ready();\nawait client.increment(undefined);\nawait host.dispose();\n```\n\n`worker.ready()` starts the process, waits for the child ready signal, and returns the stable\ntyped client. The pending worker intentionally exposes no public client. Calls through a previously\nreturned client fail fast while the worker is unavailable; Wire does not buffer requests during\ndowntime.\n\nStartup and exposure are explicit composition-site decisions. For eager startup, await\n`worker.ready()` under an owning scope or call `host.spawn(component, options)` to get the client. For Electron\nwindows, compose a forwarding controller and use `exposeWireToWindows()` with a `beforeOpen` hook\nthat awaits `worker.ready()`.\n\n## Child Side\n\n```ts\nimport { runWireComponentWorker } from '@emdash/wire/worker';\nimport { counterComponent } from './component';\n\nvoid runWireComponentWorker(counterComponent);\n```\n\nThe child helper resolves the parent IPC channel, requests bootstrap config and dependency channels,\nserves framed runtime wire messages, sends the ready signal after the component controller is\ninstalled, disposes the child scope on shutdown/disconnect, and exits with code `1` if creation\nfails. It also exits with code `1` when the component reports an unrecoverable failure through\n`fatal(error)`. The worker slot treats that exit like any other failed generation: it disconnects the\nold transport and applies the configured supervision schedule before starting a fresh process.\n\n<!-- File: agents/architecture/path-system.md (Tokens: ~504 | Category: subagent-persona) -->\n# Path System\n\n`@emdash/core/primitives/path/api` is the source of truth for portable file identity and lexical\npath operations. The detailed package docs live in\n[`packages/core/docs/path/README.md`](../../packages/core/docs/path/README.md).\n\n## Ownership\n\n- `packages/core/src/path/` owns pure structured path primitives, resource URI\n  encoding, and comparison keys.\n- Desktop placement resolution owns repository and worktree location policy. It combines the target\n  host's home directory from `files.getHomeDir`, desktop settings, per-project overrides, and UI\n  input, then sends absolute paths to the host runtime.\n- Repository destinations default to `~/emdash/repositories/<name>` with numeric suffix allocation.\n  Worktree pools default to `~/emdash/worktrees/<repo-basename>-<hash8(repo-path)>`; the workspace\n  planner adds the sanitized branch leaf.\n- Runtime and app layers own host resolution, native string conversion,\n  filesystem I/O, realpath checks, authorization, and persistence migrations.\n- Renderer code should eventually prefer `ResourceUri` or structured refs over\n  raw absolute strings, but existing consumers are not migrated by the foundation\n  module.\n\n## Representation Rules\n\n- Use `HostFileRef` for a full file address on a host.\n- Use `ScopedPath` for root-relative operations such as Git paths, watcher\n  events, tree entries, and bulk calls.\n- Use `ResourceUri` for serialized identity.\n- Use `ResourceKey` for in-memory dedupe and maps.\n- Use explicit `PathProfile` semantics instead of `node:path` behavior when\n  interpreting cross-host paths.\n\n## Future Adoption Map\n\n- Core `files` path helpers can delegate lexical operations to\n  `@emdash/core/primitives/path/api`.\n- Desktop `RuntimePath` and SSH path helpers can become adapters around\n  structured path parsing/formatting.\n- Git models can represent repo-relative file coordinates as\n  `PortableRelativePath`.\n- fs-watch can normalize native events into `ScopedPath` before batching and\n  coalescing.\n- workspace-server schemas can move from documented string conventions to\n  structured path schemas.\n- Renderer tabs, tree nodes, Monaco models, comments, and view-state snapshots\n  can migrate from raw path strings to `ResourceUri`.\n\nDo not add filesystem I/O, `realpath()`, host registry state, or app-specific\nauthorization to `packages/core/src/path/`.\n\n<!-- File: agents/README.md (Tokens: ~508 | Category: subagent-persona) -->\n# Agent Docs\n\nThis directory is the system of record for agent-facing repo guidance. Keep topic pages small, specific, and mechanically checkable where possible.\n\n## Recommended Reading Order\n\n1. `quickstart.md`\n2. `architecture/overview.md`\n3. the task-specific page for the area you are changing\n\n## Directory Layout\n\n- `architecture/`\n  - system structure and major code ownership boundaries\n  - [`core-modules.md`](architecture/core-modules.md) — Core runtime, service, and primitive module types, platform surfaces, dependency rules, and subpath exports\n  - [`workspace-server.md`](architecture/workspace-server.md) — protocol versioning, negotiation handshake, and behavior-change rules for the remote workspace daemon\n  - [`state-ownership.md`](architecture/state-ownership.md) — desktop-owned entities, host-owned resources, bindings, and the placement rule for operation behavior\n  - [`acp-runtime.md`](architecture/acp-runtime.md) — ACP runtime, session manager, connection pool, cells, live models, and API contract ownership\n  - [`git-runtime.md`](architecture/git-runtime.md) — Git contract/runtime ownership, session lifecycle, and nested wire composition\n  - [`path-system.md`](architecture/path-system.md) — host-aware path identity, resource URI ownership, and future migration boundaries\n- `workflows/`\n  - task-oriented procedures like testing, worktrees, remote development, and Nx task orchestration\n- `integrations/`\n  - provider, MCP, and external service guidance\n  - [`integration-plugins.md`](integrations/integration-plugins.md) — integration auth and credential contracts, stable account identity, issue-, pull-request-, and repository-plugin contracts, remote-to-integration routing\n- `risky-areas/`\n  - places where incorrect changes are expensive\n- `conventions/`\n  - coding contracts and repo rules\n  - [`ui-kit.md`](conventions/ui-kit.md) — which UI kit to use: `@emdash/ui` for components, feature Tailwind for layout one-offs, where new primitives go, `cn()` location, theming\n  - [`ui-styling.md`](conventions/ui-styling.md) — vanilla-extract styling rules for `packages/ui`\n\n## Maintenance Rules\n\n- Prefer one page per concrete topic.\n- Avoid volatile counts unless you can verify them cheaply.\n- Link to the source-of-truth file paths.\n- Update the smallest relevant page instead of expanding `AGENTS.md`.\n\n<!-- File: apps/emdash-desktop/src/core/primitives/commands/README.md (Tokens: ~118 | Category: command-prompt) -->\n# Commands\n\nCommands are portable, schema-backed action definitions. Definitions contain\nstatic metadata and optional keybindings; view scopes provide their runtime\navailability and implementation.\n\n```ts\nconst createBranchCommand = defineCommand({\n  id: 'task.createBranch',\n  title: 'Create Branch',\n  category: 'Git',\n  input: z.object({ branchName: z.string(), baseRef: z.string() }),\n});\n```\n\nCommand input describes arguments supplied for one invocation. Scope params\ndescribe the context in which the command runs; callers must not duplicate\nscope context in command input.\n\n<!-- ... +9 more modular skills available via /api/skills/generalaction/emdash?path={path} -->\n</yakaai_skills>"}