Deployment Modes
From spec-memo Wiki, the living architecture & domain knowledge base
Feature Overview [edit]
spec-memo supports three vault-global deployment modes configured in ~/.spec-memo/config.json ($SPEC_MEMO_ROOT when set). Mode lives only in that config: every agent host (Cursor, VS Code, OpenCode, Antigravity, Claude Desktop, generic stdio) runs the same memo serve, so switching modes never edits host MCP files.
- Local (default): all records, indexing, and tools run against the local filesystem vault. No network required. When
modeis omitted, effective mode islocal. - Hybrid: the local vault stays authoritative;
bootstrapbest-effort pulls remote changes for the cwd-boundprojectId, mutating tools debounce-push changes, andmemo syncperforms an explicit two-way round trip against a remote SSE daemon. Offline is fail-open: local writes succeed and dirty state plus cursor are retained for retry. - Remote: the stdio
memo serveprocess is an MCP proxy to the remote daemon/sseendpoint. No localprojects/records are written. Fail-closed: tool calls error withREMOTE_UNREACHABLEwhen the daemon is down.
Operators configure with memo setup, inspect with memo doctor, and force a sync with memo sync. Agents reach the same behavior through the MCP surface; the mode is invisible to tool callers except for the proxy/daemon hop.
Business Rules & Logic [edit]
- Mode enum is
local | hybrid | remote; an unknown value is rejected bymemo setup. modeomitted in an existing or new config resolves tolocal(backward compatible).memo doctordoes not report a missingmodeas corruption.hybridandremoterequireremote.url. Non-interactive setup without--url(and no existing valid URL) exits non-zero; interactive TTY setups prompt.- URL normalization stores the daemon origin only (scheme + host + port). Inputs such as
http://host:3000/sse,http://host:3000/message?token=abc, or trailing slashes reduce tohttp://host:3000. Onlyhttp:/https:are accepted. - Bearer tokens are read only from
SPEC_MEMO_AUTH_TOKENorSPEC_MEMO_SSE_TOKEN. They are never written toconfig.json,hybrid-state.json, vault-git commits, orexport-vaultarchives. Setup verifies token presence after saving the URL and exits non-zero when absent. localmode ignoresremote.urlat runtime even when present; doctor may display it as informational.- In
remotemode,memo syncerrors (no local vault to sync) and CLI extrascanvas,serve-canvas,sync-vault,export-vault,import-vault,hook, andwikiexit non-zero withnot available in remote mode(REMOTE_MODE_RESTRICTIONin JSON).setup,doctor, andcheck-versionstay local. - Hybrid automatic scope is the cwd-bound
projectId;memo sync --allcovers every local project. Hybrid debounce window is 2000 ms with in-flight + pending single-flight perprojectId. Both channels never share the vault lock during HTTP fetch or git network waits. - Remote proxy exposes the same 11 MCP tools with daemon-side budget caps, redaction, and project binding; errors pass through faithfully.
Technical Architecture [edit]
- Config schema (
src/types.ts):VaultConfig.mode?: DeploymentMode,VaultConfig.remote?: { url: string }, andsync.autoSyncIntervalMinutes. Merge/default logic lives insrc/vault.ts(ensureVaultStructure,DEFAULT_VAULT_CONFIG). memo setup(src/setup.ts):normalizeRemoteUrl,isTokenConfigured,getResolvedAuthToken,generateHostMcpSnippet,writeHostMcpConfig,runSetup. Host snippets are always stdio (memo serve);--print-mcp --host <name>prints,--write-mcp --host <name>merges into the host file, and status-autostart flags are stripped bystripStatusAutostartArgs.- Hybrid client (
src/hybrid-sync.ts):pullHybridProject,pushHybridProject,syncHybrid,scheduleHybridPush,flushDebouncedPushes,clearDebouncedPushes. State persists to$SPEC_MEMO_ROOT/.sync/hybrid-state.jsonviasrc/hybrid-state.ts(dirty,lastSyncAt,lastError, per-projectcursors,dirtyProjects). Cursors are monotonic; pull cursors advance onchangeset.generatedAt, push cursors use the export snapshotgeneratedAt(not wall clock), and a dirty project pushes withsinceunset so older offline records are not dropped. - Daemon routes (
src/server.ts) on the same origin as/sse:POST /api/sync/pull({ projectId?, since? }->exportChangeset),POST /api/sync/push({ changeset, force?, dryRun?, prefer?, strategy?, cleanSidecars? }->applyChangeset), andPOST /api/sync(bidirectional{ push, pull }). All require the bearer token when configured or bound non-loopback. - Remote proxy (
src/mcp-proxy.ts):createRemoteClientopens an authenticatedSSEClientTransportat{origin}/sse;callRemoteToolwraps a single request;createRemoteMcpProxyServerandstartRemoteMcpProxyServerexpose stdio.src/mcp.tsstartMcpServerroutes to the proxy whenconfig.mode === 'remote'. Errors return{ isError: true, code: 'REMOTE_UNREACHABLE' | 'REMOTE_TOOL_ERROR' | 'PROXY_INTERNAL_ERROR' }and log under subsystemremote-proxy. - Doctor (
src/doctor.ts): reports effective mode, normalized URL,tokenConfigured(boolean), hybrid state, andGET {origin}/health. Hybrid unreachable is a warning; remote unreachable is a failure. - Provenance:
0025-deployment-modes.spec.md; hybrid cadence and daemon routing are refined by0033-vault-git-hybrid-sync.spec.md,0035-sync-conflict-reconciliation.spec.md, and0044-multi-machine-sync.spec.md. See Vault Sync for dual-channel flush semantics and SSE Transport for the daemon bind/auth contract.