Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
58b69bc
feat: add session file catalog projection
pxguan Jul 27, 2026
509120f
fix: clean up session file projections
pxguan Jul 27, 2026
9fff28c
Merge remote-tracking branch 'origin/main' into codex/session-file-ca…
pxguan Jul 27, 2026
4fbf68a
fix: refresh session outputs on file listing
pxguan Jul 27, 2026
1099fb3
Merge remote-tracking branch 'origin/main' into codex/session-file-ca…
pxguan Jul 27, 2026
801f471
fix: address session file projection review feedback
pxguan Jul 27, 2026
afa6a7d
fix: materialize session outputs on write
pxguan Jul 28, 2026
eb5232b
fix: harden session projection cleanup
pxguan Jul 28, 2026
3e089a7
feat: unify session resources and files
pxguan Jul 29, 2026
53bc24a
Merge origin/main into session resource unification
pxguan Jul 29, 2026
64337a0
fix: harden unified session file lifecycle
pxguan Jul 29, 2026
ea28d2b
docs: remove obsolete session file research
pxguan Jul 29, 2026
0d2587d
fix: address session resource review feedback
pxguan Jul 29, 2026
0af03a5
fix: ignore retired filestore filesystems
pxguan Jul 30, 2026
68e4e73
refactor: rename SessionNamespaceNode to SessionResourceFile
pxguan Jul 30, 2026
501d3f2
Keep expired Filestore paths reserved until TTL cleanup
arthur-zhang Jul 30, 2026
a7ff352
refactor: generate session resource and file identities in applicatio…
pxguan Jul 30, 2026
a5445a9
Merge remote-tracking branch 'origin/codex/issue-184-session-resource…
pxguan Jul 30, 2026
a6a361d
refactor: snapshot session skills as files
pxguan Jul 30, 2026
6bb70eb
refactor: use stable UUIDs for session resources
pxguan Jul 30, 2026
862759d
fix: avoid duplicate skill snapshot files
pxguan Jul 30, 2026
87fa1cb
Merge origin/main into session-resource-file-unification
pxguan Jul 31, 2026
1f6e134
brother18@qq.com
arthur-zhang Jul 31, 2026
3ff98c8
Merge remote-tracking branch 'origin/main' into codex/issue-184-sessi…
pxguan Jul 31, 2026
ca6194b
fix(db): 修复 main 合并后的文件资源逻辑
pxguan Jul 31, 2026
d9cc7ce
Merge updated main history
arthur-zhang Jul 31, 2026
f24fcb6
fix(db): 对齐 Session 与清理任务 UUID 边界
pxguan Aug 1, 2026
53e6b35
Merge remote-tracking branch 'origin/main' into codex/issue-184-sessi…
arthur-zhang Aug 3, 2026
d84f2a0
Merge remote-tracking branch 'origin/main' into codex/issue-184-sessi…
arthur-zhang Aug 3, 2026
1ffc250
Merge remote-tracking branch 'origin/main' into codex/issue-184-sessi…
arthur-zhang Aug 4, 2026
b5f0b91
Refactor managed agent workflows and simplify implementation
arthur-zhang Aug 4, 2026
7d06f14
Merge remote-tracking branch 'origin/codex/issue-184-session-resource…
arthur-zhang Aug 4, 2026
d895da6
feat(sessions): 支持相对挂载路径输入
pxguan Aug 5, 2026
54472b1
移除 session resource 文件的 TTL 到期清理逻辑
arthur-zhang Aug 5, 2026
2a69fa4
Merge remote-tracking branch 'origin/main' into codex/issue-184-sessi…
arthur-zhang Aug 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 49 additions & 49 deletions docs/design/be/filestore.md

Large diffs are not rendered by default.

57 changes: 27 additions & 30 deletions docs/design/be/managed-agent-skills-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,27 @@ Environment Runner 在创建 cloud managed-agent Sandbox 前完成:
3. `latest` 在启动时解析为具体 active version row。后续 catalog 的 latest 变化不会改变
已启动 Session 的视图。
4. 在一只 `sqlx.Tx` 中锁定 Session filesystem 和 namespace,确保 `/skills` 固定根存在,
并原子替换该 filesystem 中 `kind=archive`、`managed_by=skill_archive` 的 entry 集合。
替换时旧的活动投影统一写入 `deleted_at`,不做硬删除;新的投影作为新 entry 插入
并原子替换该 Session 中 `resource_type=skill_archive` 的内部 Resource 集合。
替换时旧的活动 Resource 统一写入 `deleted_at`,新的 Resource 保存具体 Skill Version UUID
5. 创建 Sandbox 后,Runner 直接启动 rclone-filestore 的五个固定 mount;multimount
在内部对 destination 执行 `MkdirAll`,Runner 不执行独立 mount preparation。
6. `/skills` 使用只读 Filestore Token,直接挂载到 `/root/.claude/skills`。rclone ready
后才启动 Environment Manager;Environment Manager 不再处理 skill。

每条 archive entry 对应一个具体 skill version zip,保存:
每条 Skill Archive Resource 对应一个具体 skill version zip,保存:

- organization、workspace、filesystem 的稳定 UUID
- `metadata.skill_source` 和 `managed_resource_uuid` 中的具体 skill version UUID;
- organization、workspace、Session identity
- `skill_version_uuid` 中的具体版本 UUID;
- 唯一路径 `/skills/<directory>`;
- archive 的 bucket、key、sizeSHA-256。
- 不复制 bucket、key、sizeSHA-256;读取时从 custom 或 built-in version row 获取对象事实

同一 filesystem 内,路径和具体 skill version UUID 都唯一。Snapshot 中两个 skill
若声明相同目录但不是同一具体版本,启动失败,不能让后一个静默覆盖前一个。

```mermaid
flowchart LR
A["Session agent snapshot"] --> B["Resolve concrete catalog versions"]
B --> C["Replace kind=archive entries in one transaction"]
B --> C["Replace skill_archive Resources in one transaction"]
C --> D["Filestore /skills virtual view"]
E["Immutable zip objects"] --> D
D --> F["rclone readonly mount"]
Expand Down Expand Up @@ -70,18 +70,17 @@ Sandbox 中同一棵树直接位于:
xlsx/SKILL.md
```

`/skills` 是真实的固定一级 directory entry;每个 `/skills/<directory>` 是
`filestore_entries` 中的 archive entry,成员则根据 zip central directory 合成,不逐个
写 entry。archive entry 只借用 catalog 对象,不复制对象,也不计入 `filestore_bytes`。
虚拟文件 UUID 由 filesystem、具体 version UUID 和成员路径确定,Runner 重试不会改变
同一节点的身份。
`/skills` 是内部 directory Resource;每个 `/skills/<directory>` 是引用具体 Skill Version
的 `skill_archive` Resource。成员根据 zip central directory 动态合成,不逐个持久化,
也不生成虚假 UUID 或 `fse_` external ID。Skill Archive 不复制 catalog 对象,也不计入
`filestore_bytes`。

List、metadata 和 ranged read 都由 Filestore 服务实现。对 `/skills` 本身、其后代,以及
以 `/skills` 为 source 或 destination 的任意 mutation 均返回 `403 permission_denied`。
HTTP 只读 Token 和 rclone `readonly=true` 构成 Sandbox 的只读边界;`/skills` 与其他
只读 mount 统一使用目录权限 `0755` 和文件权限 `0644`。

非递归列举 `/skills` 时,Filestore 直接使用 archive entry 的 `path` 返回一级 skill
非递归列举 `/skills` 时,Filestore 直接使用 Skill Archive Resource 的 `path` 返回一级 skill
目录,不下载 archive。递归列举或访问具体 skill 子树时,才按需加载并校验对应 archive。

## archive 校验与缓存
Expand All @@ -102,22 +101,22 @@ LRU 缓存压缩 archive 和目录索引。单个压缩 archive 最大 8 MiB,
首个请求的取消信号,避免 leader 断开导致其他等待者一起失败;每个调用者通过自己的 context
独立等待,取消只会结束该调用者,不会终止或 Forget 仍可服务其他请求的共享任务。所有调用者
都取消后,共享任务仍允许在超时内完成并填充缓存。失败结果不写缓存,后续请求可以重新加载。
archive entry 仍是每次请求的授权事实来源;Session entry 删除后,缓存中残留的字节无法再通过
Skill Archive Resource 仍是每次请求的授权事实来源;Resource 删除后,缓存中残留的字节无法再通过
Filestore 路径访问。

## 生命周期与对象保留

Session filesystem 删除后沿用现有有界 cleanup job。最后一批普通文件退休后,同一事务
软删除该 filesystem 的 directory 和 archive entries。archive entry 只是借用 catalog
archive,不会产生 Filestore 对象清理任务或容量扣减。
Session filesystem 删除后沿用现有有界 cleanup job。最后一批 Owned File 退休后,同一事务
软删除该 Session 的内部 directory 与 Skill Archive Resources。Skill Archive 只引用 catalog
version,不会产生 Filestore 对象清理任务或容量扣减。

Runner 每次全量替换 `/skills` 投影时,会在同一事务中软删除旧的活动 archive entries
并插入新集合。这样被移除或换版的 skill 投影仍可用于审计,活动读取和唯一索引只考虑
`deleted_at is null` 的记录。历史投影不拥有 catalog archive,也不会触发对象回收。
Runner 每次全量替换 `/skills` Resources 时,会在同一事务中软删除旧集合并插入新集合。
活动读取和唯一索引只考虑 `deleted_at is null` 的记录;历史 Resource 不拥有 catalog archive,
也不会触发对象回收。

删除 custom skill/version 或用 `seed-builtin-skills --prune` 软删除 built-in catalog row 时,
不立即删除 archive 对象,也不创建通用 `object_cleanup` job。原因是已经启动的 Session
可能仍通过具体 version UUID 投影借用该对象。物理 GC 必须先确认没有任何活动投影引用
可能仍通过具体 version UUID Resource 引用该对象。读路径按稳定 UUID 继续读取已软删除 version 的对象事实,不把 catalog 列表可见性当成 Session 快照可见性。物理 GC 必须先确认没有任何活动 Resource 引用
属于独立的 reference-aware catalog GC;当前实现选择保留对象,优先保证运行中 Session
的快照稳定性。

Expand All @@ -132,21 +131,19 @@ Runner 每次全量替换 `/skills` 投影时,会在同一事务中软删除
- `/mnt/skills`、`/workspace/skills` 解压目录,以及 Claude skill discovery 软链;
- Environment Manager 的 managed-agent skill 解压职责。

迁移 `00032_add_filestore_archive_entries.sql` 直接把 `archive` 加入 entry kind,增加
archive 对象与 ownership 形状约束,为历史活动 filesystem 补齐 `/skills` 根,并清除
遗留的 `skill_prewarm` jobs;整个模型不创建独立的 skill archive 投影表。迁移
`00033_validate_filestore_archive_entries.sql` 单独验证新约束,避免在替换约束的短事务内
扫描历史 rows。两张 catalog version 表仍是 archive 所有权来源,schema 不创建
PostgreSQL 外键。
迁移 `00036_unify_session_resources_and_files.sql` 把活动的旧 Archive 节点转换为
`resource_type='skill_archive'` 的内部 Resource,仅保留 path 与 Skill Version UUID。
两张 catalog version 表仍是 archive 对象事实的唯一来源,Resource 不复制 bucket、key、size 或 SHA-256,
schema 不创建 PostgreSQL 外键。

## 验收重点

- resolver 只读取 DB metadata,不在 Session 启动路径下载 archive;
- `latest` 被钉住为具体 version,archive entry 替换是全量且原子的;
- `latest` 被钉住为具体 version,Skill Archive Resource 替换是全量且原子的;
- `/skills` list、recursive list、metadata 和 ranged read 返回 archive 成员;
- checksum、路径穿越、缺少 `SKILL.md` 等损坏 archive fail closed;
- 所有 `/skills` mutation 被拒绝;
- rclone 第五个 mount 直达 `/root/.claude/skills`,destination 由 multimount 内部创建;
- Runner 不写 legacy mount metadata,E2B runtime 不创建 skill volume;
- catalog soft delete/prune 不破坏活动 Session archive entry
- Session filesystem cleanup 会软删除 archive entry,但不删除借用的 catalog object。
- catalog soft delete/prune 不破坏活动 Session Skill Archive Resource
- Session filesystem cleanup 会软删除 Skill Archive Resource,但不删除 catalog object。
110 changes: 110 additions & 0 deletions docs/research/anthropic-session-file-resource-contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Anthropic Session File Resource 合同研究

## 研究问题

核对 Anthropic 官方 Managed Agents / Sessions Resource / Files 合同,回答以下问题:

1. attach File 到 Session 时,请求与响应中的 `file_id` 是否保持原 workspace File identity,还是创建新的 session-scoped File identity。
2. `files.list(scope_id=session)` 返回什么 ID。
3. 删除与配额语义如何。

## 结论

### 1. attach 时会创建新的 session-scoped `file_id`

官方 Adding files 文档明确说明:把已上传的文件挂到 session 时,API 会为该 session 中的文件实例创建一个新的 `file_id`。也就是说:

- 请求里的 `file_id` 指向的是已存在的 workspace / Files API 文件。
- 响应里的 `file_id` 指向的是 session 内部的新文件身份,不是原始 workspace 文件身份。
- 该 session 复制件不计入存储配额。

### 2. `files.list(scope_id=session)` 返回 session 范围内的 `FileMetadata.id`

官方 Files API 文档和 SDK 都把 `scope_id` 定义为按 scope 过滤文件;`scope` 里可以是 session。返回项是 `FileMetadata`,其 `id` 是文件对象自身的标识,不是 session resource 的 `id`。

因此,`files.list(scope_id=session)` 返回的是:

- session 范围内文件对象的 `FileMetadata.id`
- 这些文件对象带有 session scope 信息

### 3. 删除语义分成两层

- `sessions.resources.delete(resource_id)` 删除的是 session resource,返回 `session_resource_deleted`。
- `files.delete(file_id)` 删除的是 Files API 的文件对象,返回 `file_deleted`。

公开文档没有把“删除 session resource”解释为“删除原 workspace 文件”。从合同层面只能确认:

- session 资源删除与 Files API 文件删除是两个不同操作。
- attach 生成的 session 复制件是 session scoped 的文件对象。
- session 复制件不计入存储限制。

### 4. 配额语义

官方文档明确给出两个限制:

- 每个 session 最多可挂载 500 个文件。
- session 中创建的文件复制件不计入存储限制。

## 公开合同 vs. 内部实现

### 公开合同可以确定的内容

- attach 请求使用原始 Files API 文件的 `file_id`。
- attach 响应生成新的 session-scoped `file_id`。
- `files.list(scope_id=session)` 返回 session scope 下的文件对象列表。
- session 资源删除和文件删除是两条不同的 API 路径。
- session 复制件不计入存储限制。

### 不能从合同直接推出的内容

- 是否必须在内部单独建一张 `files` 表。
- session 文件是否与 workspace 文件复用同一物理记录。
- 删除 session resource 时,底层存储如何实现级联。

这些都属于实现细节,只能从仓库当前代码或数据库模型判断,不能从公开合同反推为必然要求。

## 项目落地决策

本项目选择更小的内部与公开模型,明确不实现官方“attach 返回新 session-scoped
`file_id`”这一点:

- 请求 `file_id` 解析为 Source File UUID,写入 Resource 的 `file_uuid`。
- 响应继续返回 Source File ID,不生成 Alias,也不新增 File 行。
- 同一 Source 多次 attach 由不同 `sesrsc_` 和 path 区分;Catalog 按真实 File 去重。
- metadata/download 只接受真实 File ID;Source File 是唯一文件元数据与对象事实。
- Input attach 不复制对象、不计费;删除 Resource 只删除一次 Attach。
- 活动 Resource 存在时,Files delete 拒绝删除 Source File。
- `/outputs` 的真实新对象创建 Owned File,并由内部 Resource 引用。

该取舍保持 Sessions Resource 与 Files 两套删除语义清晰:
`sessions.resources.delete(sesrsc_...)` 删除挂载,`files.delete(file_...)` 删除真实文件。
代价是 attach 响应与官方当前的新 `file_id` 行为不同;这是项目当前已接受的兼容性偏差,
不应再通过隐式 Alias 模拟。

## 证据

### 官方文档

- Adding files to sessions: https://platform.claude.com/docs/en/managed-agents/files
- 文档明确写出 attach 会创建新的 `file_id`,并说明 session 复制件不计入存储限制。
- Files API reference: https://platform.claude.com/docs/en/api/beta/files
- 文档说明可以按 `scope_id` 列出某个 scope 下的文件,并支持 session scope。

### 官方 SDK 源码

- TypeScript SDK `src/resources/beta/sessions/resources.ts`: https://github.com/anthropics/anthropic-sdk-typescript/blob/main/src/resources/beta/sessions/resources.ts
- `add` 的参数要求是“已上传文件”的 `file_id`。
- 返回对象包含 session resource `id` 与 `file_id`。
- `delete` 返回 `session_resource_deleted`。
- TypeScript SDK `src/resources/beta/files.ts`: https://github.com/anthropics/anthropic-sdk-typescript/blob/main/src/resources/beta/files.ts
- `FileMetadata` 代表文件对象本身,包含 `id`、`scope` 等字段。
- `list` 支持按 `scope_id` 过滤文件。
- `delete` 返回 `file_deleted`。
- Python SDK `src/anthropic/resources/beta/files.py`: https://github.com/anthropics/anthropic-sdk-python/blob/main/src/anthropic/resources/beta/files.py
- 同样把 `scope_id` 作为 Files 列表过滤条件。
- 文件对象元数据与 scope 信息分离,支持 session scope。

## 版本备注

- 以上结论基于 Anthropic 官方当前公开的 beta 文档与官方 SDK 源码。
- 由于这是 beta 合同,后续版本可能调整字段名、响应对象或删除语义;如果升级 beta 版本,应重新核对上述三个链接。
Loading
Loading