---
title: "会话归属三层模型"
description: "说明 Peri 怎样用机器身份与路径确定会话归属：归属键是机器加路径，机器身份来自本地文件而不是硬件指纹，并给出可见入口与不保证的部分。"
---

> Documentation Index
> Fetch the complete documentation index at: https://konghayao.github.io/peri-cool/llms.txt
> Use this file to discover all available pages before exploring further.

# 会话归属三层模型

同一份代码放在两台机器上，路径完全相同。这两台机器上的会话应该属于同一个工作区吗？

v4 的答案是：不属于。会话归属由三层实体决定，路径只是其中一层。

1. **Machine**：一台机器一份身份，来自本地文件里的 UUID，不来自硬件。
2. **Workspace**：机器加路径，唯一确定一个工作区。
3. **Session**：会话就是一个线程记录，挂在某个工作区下面。

这三层的关系如下。

```mermaid
flowchart TB
  ID["本机身份<br/>环境变量 override，否则读本地身份文件<br/>UUIDv4，不是硬件指纹"]
  subgraph DB["会话存储"]
    M["machines<br/>id · 展示名 · 身份来源"]
    W["workspaces<br/>machine_id + path<br/>唯一键"]
    S["threads = Session<br/>workspace_id · 归档标记"]
    MSG["消息 / 冻结上下文 / 子线程"]
  end
  ID -->|当前机器| M
  M -->|一台机器多个工作区| W
  W -->|一个工作区多个会话| S
  S --> MSG
```

## 归属键是机器加路径

1. **唯一约束落在「机器 + 路径」这一对上**：同一台机器上的同一个路径只能有一行工作区记录，重复登记不会产生第二行，会读回已经存在的那一行。
2. **路径取的是工作区根，不是启动目录**：在 Git 仓库的任意子目录启动，归属都落到该 worktree 的根路径。
3. **linked worktree 各自算一个工作区**：它们各有自己的根路径，因此互不合并。
4. **非 Git 目录按启动目录建归属**：拿不到仓库根时，用启动时的工作目录。
5. **无法证明来源时标注为未验证**：路径来源有三种取值，未验证表示当时无法确认它确实是一个已验证的仓库根。
6. **机器是唯一键的一部分**：所以两台机器上的同一个路径是两个工作区，不是共享的一个。
7. **子会话继承父会话的归属**：子线程不会另建工作区。
8. **普通操作不改写归属**：加载、继续、列表、归档都不会重写机器、路径或工作区关联。

## 机器身份从哪里来

机器身份是一串 UUID，解析顺序固定。

1. **先看环境变量**：设置了身份环境变量且能解析为 UUID，就用它，并且**不会**写进默认身份文件。
2. **否则读本地身份文件**：默认位置在用户主目录下的 Peri 配置目录内。
3. **文件不存在时生成**：生成 UUIDv4，写入权限受限的临时文件，原子发布到目标路径；并发初始化只有一个赢家。
4. **文件存在但内容不能解析为 UUID 时报错**：不会静默生成另一份身份掩盖问题，文件原样保留。
5. **身份在进程内只读一次**：启动之后修改环境变量不会切换身份，必须重启进程。
6. **不使用硬件指纹**：主机名、MAC 地址、磁盘序列号、硬件标识都不参与推导。

第 6 条是刻意的取舍：身份要稳定到可以长期绑定归属，又不能在硬件变化、主机名变化或迁移后漂移。代价是需要你保留那份文件。

## 用户能看到什么

1. **命令行目录**：`peri meta machines` 打印当前机器 ID、每台机器的展示名与标记、每台机器下的工作区路径；加 `--json` 输出机器可读结构。
2. **只读语义**：这个命令走只读打开，不建库、不迁移、不登记；机器身份的初始化只发生在写路径，所以它不会替你把旧库顺手升级。
3. **TUI 里的入口**：会话浏览器标题显示当前机器名、完整 ID 与工作区数量。
4. **协议面**：客户端可以列出机器与工作区，归档会话需要相应能力协商。

## 身份文件丢了怎么办

1. **丢失会得到新身份**：下次启动生成新的 UUID，旧的机器行与工作区仍然在库里，但不会自动迁移到新身份下。
2. **列表如实展示新旧**：你会看到两台机器记录，其中一台是本次生成的。
3. **认领需要显式操作**：`peri machine adopt <target>` 把当前机器切换成目录里已有的某台机器。
4. **默认是干跑**：不带应用参数时，命令只打印候选机器与它名下的工作区路径，然后正常返回。
5. **应用需要三项确认**：应用参数、当前机器 ID、无活跃执行确认，缺一项就拒绝。
6. **当前机器 ID 必须匹配读取时的值**：不匹配会提示重新查看目录，避免在身份已变的情况下写入。
7. **切换后必须重启进程**：进程内已经缓存的身份无法热切换。
8. **三类目标被拒绝**：非 UUID 的目标、目录里不存在的目标、以及来源为占位身份的记录。

```mermaid
flowchart LR
  A["peri meta machines"] -->|只读打开| B["当前机器 ID + 机器列表 + 工作区路径"]
  C["TUI 会话浏览器标题"] --> D["机器名（完整 ID）· N 个工作区"]
  E["peri machine adopt 目标"] --> F{"带应用参数？"}
  F -->|否| G["干跑：打印候选机器与其工作区路径"]
  F -->|是| H{"当前 ID 匹配且已确认无活跃执行？"}
  H -->|否| I["拒绝，要求重新查看目录"]
  H -->|是| J["锁内复核原值，原子替换身份文件，提示重启生效"]
```

## 不做的事

1. **机器 ID 不是凭证**：身份文件与环境变量都不是安全凭证，不能用来做权限隔离。
2. **不承诺跨实例唯一执行**：执行所有权与多实例协调属于部署方的责任，Peri 不发执行租约，也不做实例隔离。
3. **不认领没有显式切换的旧身份**：新身份不会自动继承旧工作区。
4. **不因为目录移动而改绑旧会话**：旧归属行保持原路径，旧绑定按自身快照复核，可能失败关闭而不静默迁移。
5. **不做跨机器搬家**：改名能力目前只有内部端口，没有用户入口；跨机器继续工作的方式是新建归属的会话并显式继承历史。
6. **只读与写打开是两种能力**：写打开失败会降级为只读，浏览与进入不受影响，但新建会话会在写入前失败，不假装可写。

## 未验收与文档漂移

1. **真实远端库验收尚未执行**：压缩后的形状已接入，远端实库发布验收仍在待办列表中。
2. **端到端用例没有机器分区**：现有工作区场景用例通过隔离主目录获得各自的机器身份，但没有断言跨机器隔离。
3. **切换命令没有集成测试**：`peri machine adopt` 只在单元测试层面覆盖，命令行输出没有自动化断言。
4. **一处推断未实跑验证**：在从未写打开过的全新主目录下，只读目录命令会因身份未初始化而报错，而不是自动生成身份。这是从只读链路与身份初始化时机推出的结论，未实跑验证。
5. **文档漂移**：历史计划文档里出现过更高的代数编号，那是开发期编号；当前会话库版本以代码常量为准，不在本文引用旧编号。

## 相关实现

- 设计依据：`docs/design/storage-v2-machine-workspace-session.md`、`docs/design/session-id-environment.md`
- 身份与归属：`peri-resources/src/sessions/machine.rs`、`peri-resources/src/sessions/sqlite_store/workspace_identity.rs`
- 形状与迁移：`peri-resources/src/sessions/canonical.rs`、`peri-resources/src/sessions/storage_v11_migration.rs`
- 用户入口：`peri-tui/src/cli_meta.rs`

Source: https://konghayao.github.io/peri-cool/posts/engineering/session-ownership/index.mdx
