D0012: Daemon and Client Interfaces

用户连续执行下面几条命令时,不应该为每条命令重新扫描、解析和索引同一个 Vault:

notist search "workspace revision"
notist references vault::designs::D0011
notist check docs

编辑器与 Agent 同时工作时,也不应该因为它们分别启动了 LSP client 而为同一个 Vault 维护两份 watcher、Module graph 和向量索引。Notist 为每个活动 Vault 使用一个本地 daemon 持有长期 workspace 状态;notist CLI、LSP 和 MCP 是面向不同调用者的 client interface。

Editor          shell / Agent          AI Host
  |                  |                    |
notist lsp        notist query         notist mcp
  |                  |                    |
  +------------------+--------------------+
                     |
               local Notist protocol
                     |
              notist daemon <vault>
                     |
                VaultEngine
                     |
       Analyzer Views
             |
     WorkspaceSnapshots

这个形态类似 ADB:短命令是 client,后台进程保存可复用状态,用户不需要为每次操作重建分析服务。类比只到 client 与 daemon 的进程拓扑为止;Notist daemon 绑定一个 Vault,而不是当前用户访问过的所有知识库,也不会让所有客户端共享同一份未保存文本。

用户身份仍然定义本机 IPC 的权限边界,但不定义分析进程的资源边界。两个 Vault 的 Module tree、配置、watcher、全文与向量索引、embedding model 和权限都互不相关;把它们放进同一长期进程只会耦合内存、崩溃和升级生命周期,并没有可复用的语义状态。Notist 因而采用“每用户的私有 endpoint namespace、每 Vault 一个 daemon”的组合,而不是“每用户一个 application daemon”。

Daemon 持有什么

Daemon 是一个 Vault application core 的长期进程宿主,而不是新的语言语义层。启动时传入的 canonical root 固定它服务的 Vault;进程内只有这个 root 对应的 VaultEngine,并遵循 vault::designs::D0007-notist-toml 的 nested root 边界。Client 不能连接一个已存在 daemon 后要求它再打开任意 root。

VaultEngine 持有适合在客户端之间复用的状态:

  • 磁盘 source catalog 与文件 watcher。

  • Vault 内稳定的 FileId、ModuleId 分配与 Module tree identity。

  • 以 source content 和语义环境为 key 的 Parse、LineIndex、analysis 与 render cache。

  • 全文、向量、reference graph、摘要等可重建派生索引。

  • 对磁盘写入、rename 和索引 publication 的串行化入口。

Daemon 不成为 authored source 的真相来源。.not source 与明确的 editor overlay 仍然是输入;缓存、索引和 daemon 内存都可以在进程退出后重建。持久化索引必须记录 schema、模型、Vault identity 与 source fingerprint,版本不匹配时丢弃或迁移,不能静默解释为新数据。

全文、向量和摘要索引也不构成跨 Vault 合并进程的理由。索引的语料边界、模型配置、授权和失效判断都来自所属 Vault;跨 Vault 搜索是显式上层操作,它分别捕获各 daemon 的 snapshot,再合并带 Vault identity 与 revision 的结果,不能制造一个伪全局 index revision。

共享 Engine,不共享 Overlay

多个客户端可能同时看到同一文件的不同内容:

disk source                 A
VS Code unsaved overlay     B
another editor overlay      C
Agent default view          A

因此一个 VaultEngine 可以有多个 Analyzer View。View 保存某个 client session 的 overlay set、配置投影和 snapshot revision;vault::designs::D0011-analyzer-and-workspace-snapshot 定义的 Analyzer 与 WorkspaceSnapshot 位于这个 View 内。

VaultEngine
  shared disk sources and caches

  View(editor-1)
    overlays { file -> B }
    WorkspaceSnapshot revision 17

  View(editor-2)
    overlays { file -> C }
    WorkspaceSnapshot revision 9

  View(disk)
    overlays {}
    WorkspaceSnapshot revision 24

相同 source content 可以复用 Parse 和其他纯派生缓存,但每个 View 独立发布 snapshot。一个 View 的 revision 不能与另一个 View 比较,daemon 也不制造跨 Vault 或跨 View 的伪全局 revision。

CLI 和 MCP 默认使用 disk View,从而只读取已保存的 Vault。Agent 只有在用户或宿主显式授予 View handle 后,才能查询某个编辑器的未保存状态。View handle 是受限会话能力,不应通过进程列表、固定名称或“最近打开的编辑器”猜测。

Client Interface

所有 client interface 调用同一组协议无关的 core request。差异只在生命周期、参数投影与返回格式:

Core request              CLI                 LSP                    MCP

definition_at             query definition    textDocument/          optional tool
                                               definition

references_to             references           textDocument/          get_references
                                               references

document_symbols          outline              textDocument/          outline resource
                                               documentSymbol

search                    search               workspace/symbol       search tool
                                               only where suitable

diagnostics               check                publishDiagnostics      diagnostics resource

propose_edit              edit propose         code action/edit        propose_edit tool

CLI command 不直接扫描 Vault 或访问 parser 私有结构;连接 daemon 后,它把参数转换为 core request,再把 response 格式化为人类文本或稳定 JSON。LSP 继续负责 URI、Position、document lifecycle 与 capability negotiation。MCP 负责 Resource、Tool、Prompt schema 以及 Agent Host 的调用约定。

一个能力可以同时出现在多个 interface,但语义必须来自同一个 core operation。不能让 notist references、LSP references 和 MCP references 分别实现三套解析规则。

LSP Adapter 仍然按 Client 存在

编辑器通常要求自己启动一个 stdio language server。notist lsp 因而仍是跟随编辑器生命周期的 adapter,但它只维护协议连接和对应的 Analyzer View,不再独占整个 Vault 的磁盘状态与索引:

Editor 1 -> notist lsp -> daemon -> View(editor-1)
Editor 2 -> notist lsp -> daemon -> View(editor-2)
Agent    -> notist CLI -> daemon -> View(disk)

这避免了让多个客户端共享一条 LSP connection。LSP request ID、cancellation、client capabilities、didOpen 和 diagnostics notification 都保留在各自 adapter 内;真正昂贵的 source、cache 和 index 状态在 daemon 中复用。

如果 adapter 断开,它拥有的 View 和 overlay 在租约结束后释放。Daemon 不能把断开的未保存文本合并进 disk View。编辑器重新启动 adapter 后,应通过标准 LSP open/change lifecycle 重新提供完整 overlay。

一个编辑器 worktree 可以发现多个 Notist.toml。这不把多个 Vault 塞进同一个 daemon;LSP adapter 按最近 marker 路由 source,并维护以 canonical root 为 key 的 daemon connection:

worktree notist lsp
  ├─ vault A connection -> daemon(root A) -> View(editor, A)
  └─ vault B connection -> daemon(root B) -> View(editor, B)

Workspace symbol 等显式 worktree 级请求可以查询多个 connection,再合并仍带各自 source identity 的结果。Definition、references、diagnostics 与 overlay update 始终只进入 source 所属 Vault。

CLI 像 ADB Client 一样定位服务

普通命令先解析 canonical Vault root,再在当前用户私有的 runtime namespace 中定位这个 root 对应的 daemon endpoint:

notist search ...
  -> resolve Vault root
  -> derive endpoint from user identity + canonical root + optional managed generation
  -> lazily start notist daemon <root> when unavailable
  -> handshake protocol version, Vault root, optional generation and capabilities
  -> open disk View in the root-bound daemon
  -> execute request against captured snapshot
  -> print response and exit

普通 Vault 的同一 canonical root 只对应一个 daemon 实例。由 Notist 发布和替换内容的 managed Vault 可以额外声明 generation;此时同一 root 与 generation 组合只对应一个实例,升级前的 daemon 因 endpoint identity 不同而停止接收新 generation 的 client,并在 idle 后退出。Generation 不是普通用户 Vault 的版本号,也不能用于绕过每 Vault 隔离。Daemon 可以由 client 惰性启动,也可以由桌面应用预先启动;不同 Vault 使用不同 endpoint 和进程。单实例协调依赖操作系统 endpoint exclusivity 与本用户 runtime directory;PID file 只用于诊断,不能充当锁或身份认证。Canonical path 必须在 endpoint 派生前完成解析,避免同一目录因相对路径、symlink 或大小写表示得到多个 daemon。

Client 自动拉起的后台 daemon 在没有连接后等待一个短暂 idle grace period,再关闭 watcher、Engine 和进程;新的 client 随后可以按相同 endpoint 重新启动它。Grace period 避免连续短命 CLI 命令反复冷启动,同时不会让用户曾经访问过的所有 Vault 永久驻留。用户显式以前台方式运行 notist daemon <root> 时,进程由用户或宿主管理,不因 idle 自动退出。

Core library 保持可嵌入。CI、受限容器或显式 --no-daemon 的命令可以在进程内创建临时 VaultEngine 和 disk View,并执行相同 core request。Embedded mode 不是第二套实现;它只是将同一 service object 放在 client 进程中,因此输出语义不能因是否连接 daemon 而改变。

Local Protocol and Compatibility

Daemon 默认只接受本机 IPC:Unix domain socket 或 Windows named pipe。它不默认监听 TCP,也不因为运行在 loopback 就跳过身份边界。Endpoint 应位于当前用户拥有的 runtime directory,并使用 OS ACL 或 peer credential 限制其他用户连接。

连接首先交换:

Handshake {
  protocol_version
  client_kind
  client_version
  vault_root
  vault_generation?
  requested_capabilities
}

协议按独立于 crate API 的版本演进。Daemon 必须拒绝不能安全解释的 major version,也必须拒绝 handshake root 或 managed generation 与自身固定 identity 不一致的 client,而不是尝试把请求路由到另一个 Vault。Minor capability 通过 negotiation 发现,使旧 CLI 可以连接提供兼容 surface 的新 daemon。

每个 response 至少能关联 daemon instance、Vault identity、View identity 与 snapshot revision。长查询支持 request cancellation;notification 与 response 使用有界队列,不能让一个缓慢 client 阻塞 watcher publication 或其他 View。

Snapshot 与写入

一次 request 开始时,daemon 为它捕获一个 Arc<WorkspaceSnapshot>。即使文件 watcher 随后发布新 revision,当前 response 中的 source、range、citation 和 diagnostics 仍来自被捕获的 snapshot。

跨多次 CLI invocation 的工作流不能仅相信旧 revision,因为 daemon 可能重启,disk View 也可能已经推进。读取结果应同时提供可验证的 source fingerprint;写入采用显式前置条件:

propose_edit(view_id, base_revision, operations)
  -> plan + affected source fingerprints + diagnostics

apply_edit(plan_hash, expected_fingerprints, idempotency_key)
  -> serialize disk writes
  -> reject changed sources
  -> publish new disk snapshot

Editor View 上的未保存 overlay 不会因为 disk write 自动消失。LSP adapter 通过 watcher 或 file notification 观察磁盘变化;如果 editor buffer 已偏离旧磁盘内容,应由编辑器的正常冲突流程决定合并,而不是由 daemon 覆盖 overlay。

Failure and Restart

一个 Vault daemon 崩溃不会终止其他 Vault 的服务,也不会损坏 authored source。重启后,VaultEngine 从磁盘和有效派生索引恢复;失效缓存可以重建。所有 View、overlay、in-flight request 和未应用 edit plan 都属于进程会话状态,不能在没有明确持久化语义的情况下假装恢复。

LSP 或 MCP adapter 失去连接时应终止或重新建立 session,不能把新 daemon instance 的 revision 与旧 instance 混用。短命 CLI 可以重新连接并重试只读幂等 request;写入 request 只能携带同一 idempotency key 和 source precondition 重试。

Daemon 不应成为所有命令的单点强制依赖。Embedded mode 保留了构建、检查和修复 daemon 本身的路径;daemon 则为编辑器、Agent、preview 和检索提供长期状态与跨 interface 复用。两种运行形态共享 application core,因此部署选择不会反过来定义 Notist 语言行为。