# dsh-edge

[English](README.md) | 中文

`dsh-edge` 用一条命令把你自己的持久 DeepSeek Harness 部署到 Cloudflare Workers，之后便能从任意浏览器使用。公共 CLI 无需 checkout 源码、绑定仓库或配置构建流水线，即可完成安装与升级。

在底层，每次部署把通过认证的 owner 固定映射到一个 Durable Object，其基于 SQLite 的虚拟文件系统可跨请求持久保存。默认情况下，进程内 just-bash 后端直接在同一文件系统上执行命令，不依赖 Linux 容器或 Dynamic Worker。

`dsh-edge` 是独立的社区项目，与 DeepSeek 没有隶属关系，也未获得 DeepSeek 官方背书；DeepSeek Harness 仍是按其自身许可证使用的上游依赖。

仓库提交的 Wrangler 配置从同一套应用 graph 暴露两个部署目标。默认目标是面向 Workers Free 的 direct 模式，不包含 Worker Loader binding。命名的 `isolated` 目标会添加 `LOADER` binding，并且需要 Workers Paid，但不会 fork DSH protocol、storage、UI 或 tool implementation。

运行时保持清晰的上游责任边界：

- `ReactLoopAgent`、`AgentRegistry`、`LlmRuntime`、`ToolRuntime`、`SystemPrompt`、`SessionStore` 和 `SessionPersistence` 通过上游 Cordis 组合运行。
- 上游 `dsh-llm-deepseek` cordis 插件直接安装，自动注册 Settings 命名空间和可配置 Provider 条目。Edge 把原生 DSH `bash` 工具映射到 Cloudflare Computer。
- Durable Object SQLite 实现上游 `SessionHandle` 持久化约定。Edge 缓冲实时事件并在发布前完成落盘；初始化事件（包括冷会话切换模型时的 `session/end-seed`）先于后续变更持久化。上游 agent loop 在恢复时补齐中断 turn 的结束事件。Edge 在持有写入所有权时先截断损坏的物理尾部，并原子重建摘要和提醒状态；对于含有 inbox 事件、格式损坏提醒或已派发提醒的尾部，拒绝自动修复，避免重复投递。
- 模型历史从 canonical 事件投影，不在 Edge 中建立第二套 schema。
- `GoalService` 和 `ToolGoal` 作为上游 cordis 插件直接组合。浏览器 GoalBar mutation 经由 `TypertGatewayService` 路由。
- `SessionProjectionCache` 通过 KV 缓存在 Durable Object 重启后持久化 goal、title 和 model 状态。
- `EdgeFileSystem` 提供 read、write、edit 和 read_image 工具，底层使用 Durable Object VFS。
- 上下文压缩、token 计量和自动 session 标题作为上游 cordis 插件运行。

浏览器也保持上游所有权：

- 构建期 assembler 从上游配置推导 Web roster，注入标准 `window.__DSH_BOOT__` graph，并输出 Cloudflare 静态资源。
- Durable Object 通过标准 HTTP carrier 实现受支持的上游 `ApiProxy` 方法，并以支持休眠的 WebSocket 提供两条 downlink。
- Image composer、gallery、lightbox、attachment wire contract 与 DeepSeek serializer 全部原样复用。
- Storage seam 把新部署的图片存入有界 Durable Object storage，已固定使用私有 R2 的部署则继续使用 R2。
- 缺少对应 host domain 的客户端插件会被排除，而不会 fork UI 代码。可选的本地 host 插件目前仍不可用。
- 上游的“更多”菜单（`dsh-client-ui-entry-customization`）提供浏览插件和 Creator 草稿，它们需要 Edge 不提供的插件页面和 Creator 预设，所以该包被排除。由于会话标题栏也会把标签行放不下的视图收进它的“更多”菜单，`dsh-edge-client-ui` 自己在那里注册，只列出这些视图；侧边栏和引导页的“更多”菜单不显示。
- 使用内置模型目录和官方 API 地址时，DeepSeek provider 与上游自 0.2.1-rc.3 起一样，从 DeepSeek 的 `GET /models` 获取模型列表：Durable Object 启动后的第一次请求最多等待 5 秒，超过 10 分钟的列表会在后台刷新，获取失败时保留上一次成功的列表或使用内置目录。自定义模型列表或使用其他地址时不发起请求。
- 会话日志下载（`/export` 和会话标题栏里的下载项）保持上游的压缩包结构，但 Edge 边压缩边按页读取日志，导出占用的内存取决于每页的大小，而不是会话的长度。
- Settings → Plugins 通过上游 `dsh-host-plugin-inventory` Remote 列出当前运行的 composition。Workers 上没有 cordis Loader，因此 Edge 自有的 `EdgeLoader` 用实时的 host 插件 registry 和经过评审的 Web boot graph 来回答它注入的 `loader`。
- 一个小型 Edge 登录外壳在不修改上游 UI 和协议的前提下保护它们。

## 主会话 Runtime 队列

owner Durable Object 同一时间只准入一个活跃主会话。普通输入先持久排队再确认收到；每次驻留准入一条普通输入，上游 loop 空闲且清理完成后，有待处理输入的会话回到就绪队尾。其他 tab 可以提交并观察同一份队列。显式 steer 仍指向当前运行。Edge 浏览器把 steer/取消绑定到它观察到的 canonical turn，防止延迟操作影响新运行；旧 API 客户端可通过 `x-dsh-edge-turn-seq` 启用同样的校验。

每个 owner 最多接受 128 条未收尾输入和 2 MiB 的序列化输入数据。容量耗尽返回 HTTP 429（`QUEUE_FULL`）或 RPC `session/agent-busy`；待积压任务减少后重试。浏览器 RPC 重试复用 `requestId`（旧客户端复用 `rpcId`）；原始 SSE 调用可提供最长 128 字符的 `Idempotency-Key`。同一身份携带不同内容会失败。并发 steer 重试共享准入结果；被拒绝的 steer 再次重试仍返回拒绝，重启后也一样。若收件箱已接受变更但持久化屏障失败，则阻止模型消费并释放 steering 容量；从队列提升的输入保留原先已接受的收件状态。原始 SSE 先发送收件注释，并返回 `x-dsh-edge-input-id`；断开流不会取消排队输入，可通过 `session.updateQueue` 移除。

实际工具执行共享两个许可，最多等待 32 项，排队期限 30 秒，合作式执行期限 60 秒。工具池期限是额外的上限：bash 配置更长的超时不会延长它，交互式 `ask_user_question` / `exit_plan_mode` 也必须在此期限内结束。取消后必须等清理结束才归还许可。上游的并行/独占分类继续有效；嵌套调用继承根执行预算。后台标题流单独使用一个有界许可，保留上游超时。这些限制不能中断无界同步代码，也不会为每个工具分配独立内存。

输入请求和完成事件立即推进队列。现有 downlink 过期 alarm 同时检查持久工作；运行中的恢复检查间隔为 30 秒，alarm 每次等待一份驻留执行结束。用户主动取消会结束当前 turn，并在清理完成后继续处理排队输入。每次驻留独立获得十分钟期限，为 alarm 的平台时限留出清理余量。重启后标记中断，暂停该会话的后续输入；用户发送新消息可明确恢复待处理输入。短暂的模型错误（provider 故障、输出上限）不会暂停后续输入；仅崩溃恢复和驻留超时触发暂停。结果未知的外部工具副作用不会自动重放。冷历史订阅在交付快照后释放准备状态，不会激活 Agent。`/plan` 等 Agent 作用域命令临时使用同一个驻留预算。冷会话切换模型仍会发布上游的持久投影。不新增 subagent 或独立的 cron 执行槽。

构建并提升产物（`pnpm run bundle:workers`）后，在本包目录运行 `DSH_EDGE_PLAYWRIGHT_CHANNEL=chrome node tests/runtime-queues.probe.mjs`，可用真实浏览器 tab 本地验证。设置 `DSH_EDGE_TEST_RUNTIME_MODE=isolated` 可验证 Dynamic Worker 模式。探针使用本地模拟 provider，检查实际 HTTP 并发与取消，并关闭所有 tab 后重启 Worker 验证 alarm 恢复。

## 定时提醒

可以要求 Agent “十分钟后提醒我检查 PR”。直接复用已发布上游的 `schedule_create`、`schedule_list`、`schedule_update` 和 `schedule_delete` 工具（`dsh-tool-schedule`），管理属于当前会话的提醒；被委派的 subagent 不能使用它们。与上游标准预设一样，每个模型步骤还会收到一条 `dsh-time-context` 读数：当前时间、浏览器时区，以及距上一条消息的时长，最多每十分钟一次。不带浏览器时区的请求（例如送达的提醒）会告诉模型时区不可用。支持的规则与上游一致：延迟 `after_seconds`、带明确偏移或时区的绝对时间 `at`、固定间隔 `every_seconds`（最短 60 秒），以及在明确 IANA 时区下的每日、每周和五字段 cron 规则。上游的任务页面（`dsh-client-ui-schedule`）列出所有提醒及其所属会话和状态，可以修改名称、指令和时间、删除提醒，并查看它已经送达的记录。页面通过上游的 `schedule` Remote 命名空间工作，Edge 用 Durable Object 存储提供它；提醒被创建、修改、删除或送达时页面会刷新。

Edge 用 Durable Object alarm 替代上游仅在活跃 Agent 中存在的 timer。到期后会自动恢复冷会话，即使所有浏览器 tab 都已关闭。提醒进入现有持久输入队列，共享唯一主会话槽和并发为 2 的短工具池。不取消或 steer 正在执行的任务，已排队消息保持原有顺序。提醒留在原会话中，要求模型呈现提醒内容，而不是把它当作新的用户执行指令；不发送邮件、推送或外部通知，也不创建新 session。

提醒以任务形式保存在 Durable Object SQLite 中，并带有到期时间索引。送达一条提醒时，入队、推进或结束任务、保存送达记录在同一个 SQLite 事务中完成，alarm 也在该事务中设置。每次送达记录发生时间、送达时间、消息和 prompt；与上游一样，每个任务保留距最近一次送达 30 天内最新的 200 条，删除任务时一并删除。到期的一次性提醒优先于周期提醒批次；错过多个周期时，每条周期提醒只呈现最新一次，并直接推进至未来的下一次时间。每个 owner 最多保留 128 条活跃提醒和最新的 128 条已结束提醒，每条 prompt 的 JSON 编码预算为 4 KiB。提醒入队前先预留队列容量；容量不足时，先让用户任务排空。提醒准入失败后按会话退避 30 秒，重试时间跨重启持久保存。失败会话不会阻塞健康会话的到期提醒或排队输入；即使容量仅被暂停输入占用，也不会高频唤醒。崩溃后有待处理输入且已暂停的会话，仍需用户发送新消息恢复；提醒不会覆盖该暂停状态，也不会重放中断的外部副作用。送达记录只确认已入队，不表示模型执行成功或用户已读。

构建后，在此包目录运行 `DSH_EDGE_PLAYWRIGHT_CHANNEL=chrome node tests/schedule.probe.mjs`，验证忙时顺序、删除提醒及 Worker 重启后的 alarm 恢复；设置 `DSH_EDGE_TEST_RUNTIME_MODE=isolated` 验证 Dynamic Worker 模式。浏览器快照测试还会通过原生输入框创建提醒、关闭页面、仅观察模型服务而不请求 DO，再打开会话确认投递结果。

## 快速导航

- [子系统 wiki：架构与路线图](https://github.com/pawaca/dsh-edge/wiki)
- [在 Cloudflare 上安装或升级](#安装到-cloudflare)
- [对比原生复用、已适配与未支持能力](#cloudflare-兼容矩阵)
- [配置 DeepSeek 凭据、模型、超时与 owner 认证](#api-key-边界)
- [在本地运行发布 runtime](#本地运行)
- [查阅路由、限制与持久化行为](#edge-api)

## 本地运行

使用 Node.js 22.19 或更高版本。在仓库根目录安装仓库依赖，以及使用独立 lock 的发布装配依赖：

```sh
pnpm install --frozen-lockfile
pnpm --dir apps/dsh-edge/standalone install --frozen-lockfile
```

调用 DeepSeek 前，创建一个不会提交到 Git 的 `apps/dsh-edge/.dev.vars` 文件：

```dotenv
DSH_EDGE_ACCESS_KEY=replace-with-at-least-32-random-bytes
DEEPSEEK_API_KEY=replace-with-your-key
DEEPSEEK_MAX_OUTPUT_TOKENS=256000
DEEPSEEK_MODEL=deepseek-flash
DEEPSEEK_REASONING_EFFORT=high
DEEPSEEK_STREAM_IDLE_TIMEOUT_MS=120000
DSH_EDGE_DEFAULT_COMMAND_TIMEOUT_MS=120000
DSH_EDGE_MAX_COMMAND_TIMEOUT_MS=120000
```

然后启动 Worker：

```sh
pnpm --filter dsh-edge dev
```

该命令会把锁定版本的 Harness 发布包构建为与安装器交付内容相同的预构建 Web 与 Worker 产物，然后让 Wrangler 在不重新打包的情况下启动这些产物。打开输出的地址（通常为 `http://localhost:8787`），输入 owner access key，选择 **Workspace** 并发送消息。Web UI 会创建 lazy blank session，并通过已认证的 Durable Object WebSocket 流式接收该轮次。

诊断 API 使用相同的 owner cookie。先登录一次并把 cookie 写入临时 cookie jar，再验证持久文件系统和 shell：

```sh
curl -c /tmp/dsh-edge-cookie -X POST \
  -H 'content-type: application/x-www-form-urlencoded' \
  --data-urlencode 'accessKey=replace-with-your-random-key' \
  http://localhost:8787/api/auth/login

curl -b /tmp/dsh-edge-cookie -X PUT --data 'hello from the edge' \
  'http://localhost:8787/api/workspace/file?path=/workspace/hello.txt'

curl -b /tmp/dsh-edge-cookie -X POST -H 'content-type: application/json' \
  --data '{"command":"cat /workspace/hello.txt"}' \
  http://localhost:8787/api/workspace/exec
```

如需持久对话，先创建 session，再把返回的 id 用于后续 turn：

```sh
curl -b /tmp/dsh-edge-cookie -X POST -H 'content-type: application/json' \
  --data '{"title":"Edge session"}' \
  http://localhost:8787/api/sessions

curl -b /tmp/dsh-edge-cookie -N -X POST -H 'content-type: application/json' \
  --data '{"message":"Read /workspace/hello.txt and remember the result."}' \
  http://localhost:8787/api/sessions/SESSION_ID/turn
```

诊断型 session API 保留上游 event，同时为每次读取设置边界：

- Turn 直接把上游 `SessionEvent` 作为 SSE 返回，包括 inbox splice、assistant chunk/message、tool call/result 及 turn/step 边界。
- Live stream 每个客户端最多排队 1 MiB。读取过慢的客户端会断线，但不会取消 turn 或持久化。
- Session detail 返回有界 metadata。Event replay 默认 128 条、最多 256 条，会预检持久字节，最多保留 1 MiB 编码后的 SSE，并返回 continuation header。
- Session listing 默认 50 条、最多 100 条。Durable Object 从 canonical row 推导标题与时间，不加载每个 log；上游 Web 还会收到 retained blank header。
- Browser history 最多 50 条消息，并拒绝而非截断超过 8,192 个 event 或 8 MiB 的 window。Cold 路径在 SQL 应用边界；live 路径不复制完整内存 log 就定位边界。
- 侧边栏搜索直接使用 canonical current user/assistant message，不引入第二套索引或 wire format。它最多检查 32 个最近 session，要求完整 log 不超过 512 个 event，cold 时不超过 256 KiB，并最多返回 20 个 snippet；达到边界时返回 `hasMore`。
- 模型查找与选择只做 header point read。只有恢复 agent 的 turn 才解码 canonical history。

每个已认证请求都使用该部署固定的 `owner` Durable Object。旧 `x-dsh-edge-instance` 请求头与 `instance` 查询参数会被拒绝，不会被当作 identity。`/api/sessions/SESSION_ID/turn` 会延续已保存的 canonical history。

## Cloudflare 兼容矩阵

该参考区分三类代码：可在 Workers 原生运行、需要在现有 DSH capability 上适配， 以及仍然依赖本地 Node.js host。表中的“当前”专指 `apps/dsh-edge`，不代表未来所有 Cloudflare 工作的最终状态。

| 能力 | 上游实现 | 当前 edge 状态 | Edge 决策 |
| --- | --- | --- | --- |
| DeepSeek transport | Fetch、SSE 解析、wire translation 和 retry metadata | 复用 | 每个请求构造一个上游 `DeepSeekAdapter`；其兼容的 Node API 由 `nodejs_compat` 提供。 |
| Provider attribution | 用 Node `createRequire` 加载 package version | 完成可移植修复后复用 | 静态导入 package metadata，让 bundler 保留同一个版本来源，运行时不再依赖 `import.meta.url`。 |
| LLM protocol | DSH messages、content blocks、stream chunks、tool calls、usage 和 finish reasons | 复用 | 由上游 `LlmRuntime` 和 `ReactLoopAgent` 组装、流式处理并记录 model exchange。 DeepSeek 由 `dsh-llm-deepseek-api-key`（`llm-deepseek`）提供。其他服务商与上游一样在"设置 → Models"页面添加，通过 `dsh-llm-pi-ai`（`llm-pi-ai`）接入：支持 OpenAI chat completions、OpenAI responses 和 Anthropic messages 协议，Base URL 可以是任意不带账号密码、查询参数或片段的 HTTP(S) 地址（OpenRouter、Cloudflare Workers AI 或 AI Gateway、自建网关），并可从端点发现模型。保存的路由无需重启即可用于对话，可以在模型菜单中选择，可以作为新会话的默认模型，也可以给子 agent 使用。依赖本地登录或读取本地凭据文件的服务商（GitHub Copilot、OpenAI Codex，以及通过本地凭据使用的 Amazon Bedrock 或 Google Vertex）不提供。 |
| Agent loop | 由 Cordis 组合、带 hooks、guards、sessions 和 tools 的 `ReactLoopAgent` | 复用 | 通过 `AgentRegistry` 创建和冷恢复 agent。上下文压缩、token 计量、工具结果修剪、自动 session 标题生成、重复工具调用提醒（连续 3、5、8 次完全相同的调用时提醒）、模型请求重试（遇到限流、服务端错误、超时、网络错误或空响应时，在同一轮内最多重试 5 次，等待 0.5–10 秒，或按服务端的 `Retry-After`），以及持久化检查点（每次模型请求、每次顶层工具调用和每一步之前先保存 session 日志），作为上游 cordis 插件运行。上游的 `/compact` 命令（`dsh-command-compact`）按需压缩会话历史。 |
| Bash tool | Node subprocess、sandbox、terminal 和 job services | 在原生 tool seam 上适配 | 注册上游 `ToolDefinition`，但通过配置的 Computer workspace backend 和 just-bash 执行其 body。默认 direct backend 在 owner Durable Object 内运行，启用 hardened interpreter limits 且不提供网络命令；添加 `LOADER` binding 后会选择 Computer 的 isolated Worker Shell backend。原生 tool cancellation 会通过 Computer execution handle 发送 `SIGINT`。部署配置提供明确的默认 timeout 与调用方可选值上限，`timedOut` 则独立于 exit 与 cancellation status 报告 deadline。不支持原生二进制、后台进程、PTY 和任意 Linux 行为。bash 工具没有 `run_in_background`，也不会把耗时命令转为后台任务。 |
| Workspace filesystem | 本地 filesystem services 和 host paths | 适配 | 在 owner 基于 SQLite 的 Durable Object VFS 中保存 `/workspace`。 |
| Session persistence | `SessionPersistence` service、`PersistenceCoordinator` 及本地 JSONL/SQLite backends | 原生 backend 适配 | 复用上游 service 及 coordinator 的职责划分，在 Durable Object SQL 上实现存储原语，并使用上游 header/event 映射。一个 Edge 独有表会在透明休眠期间保留 empty session header，并在 canonical rows 物化时删除；Edge 不定义 turn 或 message schema。内部 coordinator helper 负责校验有界 replay loader，并在 disposal 前放弃失败且尚未物化的创建。它用上游自己的检查执行上游的存储契约：读取时拒绝本版本不认识且写入方未标记为 ignorable 的事件类型；以写方式打开时也会拒绝，并释放占用：每个会话的摘要行记录它已存的事件类型，由本来就会写这一行的追加更新，因此打开时只读一行（早期版本存储的会话，或之后被修复、迁移过的会话，会扫描一次并记录类型）；追加时只存储已检查过的无损 JSON 快照，且必须接续已存日志；有界的 `read(offset, length)` 只选取所需的行，范围内无法解析的行按数据损坏报告。该契约有两处不同：已创建会话的 header 行在 create 时写入，而不是在第一次追加或 flush 时；空的已创建会话作为保留的 blank 保存，因此 flush 不会让它持久化。 |
| Settings and credentials | 基于文件的 settings、launch environment 和 credential services | 可写，使用 DO 存储 | 上游 `dsh-settings` 插件通过 `DurableObjectSettingsProvider` 把 user section 持久化到 Durable Object KV。上游 `dsh-llm-deepseek` cordis 插件直接安装，自动注册 `llm-deepseek` Settings 命名空间和可配置 Provider 条目；Settings → Models 页面可在运行时读取和修改 Provider 配置（base URL、model catalog、API key reference、reasoning effort），无需重新部署。`EdgeCredentialProvider` 先从 DO KV 解析凭证，然后降级到 Worker 环境变量 secret；`set`/`unset` 通过上游接缝持久化，并与上游的 provider 一样，把每次已保存的修改发布为 `credentials/reference-updated`，因此已打开的 Models 页面、模型菜单或网页搜索卡片无需刷新页面即可更新。部署默认值（maxTokens 256,000，reasoningEffort high）与上游插件对齐，未设置部署变量时与上游行为一致。 Settings → 内置插件还有一个“设置”标签页，提供上游的 Agent 循环和网页搜索两张卡片：Edge 注册它们的 `agent-loop`（并行工具调用数）和 `web-search-deepseek`（每次请求的搜索次数、接口地址，以及搜索使用的 DeepSeek Key）命名空间，把保存的值应用到正在运行的插件，并拒绝 `DEEPSEEK_SEARCH_BASE_URL` 同样会拒绝的接口地址。上游的 Shell 卡片编辑 `bash-sandbox` 命名空间，Edge 为自己的 bash 注册了它：命令超时默认取 `DSH_EDGE_DEFAULT_COMMAND_TIMEOUT_MS`，不能超过 `DSH_EDGE_MAX_COMMAND_TIMEOUT_MS`；单流输出上限（默认 64,000 字节，范围 1 KiB 到 1 MiB，因为每个流都要在内存中保留这么多）是输出转存前内联保留的尾部；两者都从下一条命令起生效，两种 shell 和 HTTP exec 路由都适用。上游的子 agent 卡片编辑子 agent 运行时的 `subagent` 命名空间：最大递归深度（默认 1）和常驻子 agent 的并行上限（默认 8）；卡片的选模型部分编辑 `subagent-model-selection-settings`：与上游一样默认关闭；开启后，新会话会记录一次允许的模型（一条 `subagent/model-selection-policy` 事件），其 `subagent` 工具会多出 `list_subagent_models`，并可让子 agent 使用其中的模型运行。Edge 的会话在首次使用时恢复，所以 Edge 在尚未运行过 turn 的根会话上记录该设置，而上游在从未加种子的会话上记录。`subagent_fork` 的子 agent 沿用父 agent 的模型。上游承载这些卡片的插件管理器页面没有提供，因为它通过 Edge 没有的运行时 Loader 安装和卸载插件。 |
| Host boot and plugins | Node 命令行、Cordis profile loading、package resolution 和 HMR | 显式 Edge composition | 不在 Workerd 中运行本地 boot profile。部署前构建 immutable 客户端包，并排除 HMR 及 Edge `ApiProxy` 未暴露的 host domain。上游 `dsh-host-plugin-inventory` Remote 为 Settings → Plugins 提供数据；`EdgeLoader` 从实时插件 registry 投影带 fiber phase 的 `host:` 条目，并从 `expected-boot-graph.json` 投影 `web:` 条目。 |
| DSH transport | Typed HTTP RPC 加 mux/host WebSocket downlink | 复用并提供 Edge 服务端实现 | 对 unary method 使用上游 fetch carrier，并保留其 envelope、schema、projection、lazy blank-session 行为、有界内容搜索、prompt 与 queue mutation、workspace mutation、queue snapshot 和 event frame。两条 downlink 都由 Durable Object WebSocket 休眠机制持有；mux 重连会重放 live inbox 的待处理状态，REST/SSE 路由则保留为诊断兼容路径。 |
| Workspace registry | Storage-domain global state 加 `WorkspaceRecord` rows | 原生 backend 适配 | 保持上游 global 和 record value shape，包括手动 session 顺序与 archive membership；仅把物理 key 和原子写入映射到 Durable Object storage。Edge 把 registry 限制为一个原生 `/workspace` VFS；rename、delete、recreate 与 session reorder 保持上游 RPC 和 Host-frame 语义。 |
| Directory picker | `ctx.directoryPicker` seam，由 `dsh-host-directory-picker-auto` 在启动时选择 `-native` OS 选择器或 `-browse` 文件系统 backend，加 Web browse 对话框 | 复用并提供 Edge browse backend | 安装上游 seam，并把上游 `dsh-client-ui-directory-picker-browse` 对话框固定进 Web roster；Edge 从 `/workspace` Computer VFS 按每次一层回答 `browse` capability，以 `/workspace` 为根并沿用上游的 hidden、bound 与失败词汇，因此上游 `DirectoryPickerController` 提供 `directoryPicker/list` 与 `createDirectory`，并拒绝原生 `pick`。 |
| 对话文件链接 | `session/openWorkspacePath` 经 `dsh-native-command` 把点击的路径交给宿主桌面打开器；`session/canOpenWorkspacePath` 决定是否展示该入口 | 在上游 `SessionControllerInternals` seam 适配，加 Web 端下载回退 | 用 Edge internals 组合 `SessionController`，让原生探测返回 false、打开尝试以可读原因失败而不是 `child_process` 错误。V3 Web 客户端在右侧栏预览文件链接。Edge 将上游 `workspaceFiles` 控制器接入请求作用域内的 VFS 读取，支持有界文本/字节分页和元数据订阅。旧版 `ctx.remote.session.openWorkspacePath` 回退以及经 owner 鉴权的 `GET /api/workspace/file` 下载仍然可用。 |
| Existing Web UI | 运行时加载的 shell 和 `dsh.client` 插件 graph | 复用并采用通用 composition fallback | 把上游 shell 和受支持的上游客户端包组装成 Worker 静态资源；共享的 slot occupancy 规则会隐藏缺少 provider 的 action。Cloudflare 直接提供普通资源，`/`、`/login` 与 `/api/*` 则进入 Worker 执行 owner access control。组装后的 asset policy 会阻止所有直接或 SPA-fallback shell alias 被嵌入 frame。 |
| Other tools | Web Search、filesystem editor tools、MCP、skills、workflows、jobs 和 subagents | Search、文件、glob/grep、goal、todo、skill、subagent 和 MCP 工具已移植；workflow 与 run_code 已在 isolated 构建中移植 | 复用上游 DeepSeek Web Search 及其 30 秒 tool-call timeout。网页搜索卡片里的“Use the new Web Search API”开关（默认关闭）会把搜索和抓取切换到 DeepSeek Search and Fetch API（`dsh-web-deepseek-api`）：页面由 DeepSeek 抓取，Worker 自己不读取任何第三方页面，也不产生辅助模型请求。它们使用 Models 页面保存的 DeepSeek API key（key 名称不能是其他 Edge 功能的凭据，设置里不接受明文 key），API 地址必须是不含凭据、查询或片段的 HTTP(S) URL，重定向会被拒绝，也不提供账号登录路径。Search and Fetch API 可能与模型 token 分开计费。上游 `workflow` 工具仅由 Dynamic Worker 运行时提供（isolated、Paid 计划构建）；Direct 构建不注册该工具。由于 workerd 禁止在进程内使用 `eval`/`new Function`/`node:vm`，每次运行都会把脚本加载到独立的 Dynamic Worker 中执行，该 Worker 无出站网络，并受 30 秒 Worker Loader `cpuMs` 上限约束（由 Cloudflare 运行时强制执行，本地开发环境不执行）；`agent()`、`phase()`、`log()` 经 RPC 桥回到 Durable Object，由其执行各项上限：同时最多运行 4 个子 agent（低于 Workers 六连接上限），单次运行最多启动 100 个子 agent，以限制子会话的行写入。同一运行时还提供上游的 `ralph` 工具（`dsh-tool-ralph`）：它在前台循环启动全新的 `subagent` 子 agent，朝同一个目标推进，每一轮只把一份有大小上限的报告交给下一轮，直到某个子 agent 报告完成或遇到阻碍。与上游一样，它的提示词要求只在用户明确要求时使用。它运行在 workflow 引擎上，因此一次最多 100 轮（上游默认 256 轮）；与 `workflow` 一样，PTC 模式预设不提供它。上游 `run_code` 工具（`dsh-tools` PTC 模式）由同一运行时提供，但只提供给以上游 `ptc` agent 预设（PTC 模式）开始的会话。isolated 部署沿用上游 id 提供该预设，因此 Web 客户端显示其内置的双语名称。该预设通过生成的 SDK 呈现全部工具，并与上游一样不提供 `workflow`，因为 `run_code` 是它唯一的编排方式。默认使用上游的 `standard` 预设（标准模式；以早先 `dsh-edge` id 记录的会话按 `standard` 读取），以原生方式呈现工具，并保留 `workflow`。与上游一致，Settings → Agent presets 写入 `agent-preset-registry` 设置命名空间的 `selectedDefault`，从可用预设中选择新会话的起始预设；早期版本保存在 `agent-presets` 下的默认值会在启动时迁移过来。在 PTC 模式下，每个程序先用 sucrase 去除类型，再在独立 Dynamic Worker 中运行，受 30 秒 `cpuMs` 上限约束且无出站网络；其中的 `tools.*` 调用经 RPC 桥回到 Durable Object，由其限制为每次运行最多 200 次（每次都是一次写入 session 事件的嵌套工具派发）、参数最多 256 KiB、单次结果最多 2 MiB，嵌套调用仍走审批。与上游 Node 运行时描述自身环境一样，`run_code` 的描述会把这个环境和这些上限告诉模型。文件工具（read/write/edit/read_image）通过 `EdgeFileSystem` 适配 Computer VFS。`glob` 和 `grep` 保持上游 `dsh-tool-fs-search` 的工具名、参数结构、结果文本、搜索卡片，以及超过 100 条路径或 250 条匹配时把完整结果另存为文件；但 Workers 无法启动它所需的 ripgrep 程序，所以 Edge 在 VFS 上运行它们：两者都跳过版本控制元数据目录；`grep` 还跳过二进制文件和超过 4 MiB 的文件，并与上游 `grep` 沿用的 ripgrep 默认行为一致，跳过隐藏条目，以及 `.rgignore`、`.ignore` 和（在 git 仓库内的）`.gitignore` 文件排除的条目，且不进入被忽略的目录，因此一个很大的被忽略的 `node_modules` 只占一个条目（与 ripgrep 的 `--glob` 一致，`include` 匹配的文件即使被忽略或隐藏也会被搜索）；`glob` 与上游一致（`--no-ignore --hidden`），会列出隐藏和被忽略的文件。ignore 规则按字符匹配，而 ripgrep 在 Unix 上按字节匹配 `?` 和字符类，所以像 `?` 这样的规则也会匹配只有一个非 ASCII 字符的文件名。不读取全局 gitignore 文件和 `.git/info/exclude`，超过 256 KiB 的 ignore 文件，或会让一次搜索编译的匹配器超过 5,000 个（每条规则一个，带 `{a,b}` 分组的规则按展开后的每个分支计，遍历期间都保留在内存中）的 ignore 文件不生效（预算按遍历顺序分配，所以预算用完后遇到的 ignore 文件即使优先级高于先前的 `.gitignore` 也会被跳过，而 ripgrep 会应用它）；一次遍历若要测试 ignore 规则超过 5,000,000 次，会像超出条目上限一样失败。`grep` 的正则使用 RE2 语法（不支持环视和反向引用），由 re2js 以线性时间匹配，所以病态正则不会卡住 Durable Object。一次搜索最多遍历 20,000 个条目，`grep` 最多收集 20,000 行匹配或 20 MB 文本；超出上限时会失败，并提示缩小路径或模式。ripgrep 15 会拒绝的 glob 语法（未闭合的 `[` 或 `{`、`[z-a]` 这样的反向范围、末尾悬空的 `\`）会作为无效模式报错；`a{b,{c,d}}` 这样的嵌套分组可以使用。搜索在每次列出目录前后检查是否已取消，所以 30 秒的工具超时能中止耗时的遍历。与上游一致，文件观察策略（`dsh-fs-observation-policy`）为它们把关：`write` 不能覆盖本会话尚未读过的文件，`edit` 必须先读过目标文件，读取之后被改动过的文件（例如被 `bash` 或 Linux 容器改动）在重新读取之前会作为过期而失败；会话重启后不保留读取记录。Goal 工具（`ToolGoal`）作为上游 cordis 插件直接与 `GoalService` 组合。子 agent 沿用上游标准组合：`subagent` 启动一个全新的子 agent，`subagent_fork` 启动一个从父 agent 已完成的 turn 开始的子 agent（`dsh-subagent-spawn-in-process`、`dsh-subagent-fork-in-process`）。两者默认都在后台以可续接子 agent 运行，父 agent 用 `send_message` 继续它、用 `interrupt_agent` 停止它当前的 turn、用 `list_agents` 找到它（`dsh-tool-subagent-control`）。父 agent 的 turn 结束后子 agent 仍继续工作，它的结束消息会唤醒父 agent 再跑一个 turn；与上游一致，停止父 agent 不会停止正在运行的子 agent。未在运行的子 agent 收到消息时会从存储恢复，重启后也一样。上游 `todo_write` 工具（`dsh-tool-todo`）维护会话的任务列表，与上游的组合一样允许多个任务同时进行；它的 `todos` projection 驱动 Web 客户端的待办面板。上游 `present` 工具（`dsh-tool-present`）把工作区文件声明为本 turn 的交付物，Web 客户端将其显示为可在侧边栏预览的文件卡片。上游交付物的 Host 半边（`dsh-client-ui-deliverables`）原样挂载：它提供卡片使用的 `/api/present.*` 和 `/api/changes.*` 路由，并在系统提示中加入关于链接文件和使用 `present` 的说明；Edge 没有桌面，所以卡片不提供用本机应用打开。每个顶层 turn 中的 `write` 和 `edit` 调用会生成变更文件卡片，列出每个文件的增删行数，并可查看 turn 开始与结束时的对比。这是上游 `dsh-workspace-changes` 文档中的无 git 模式（`edge-workspace-changes.ts`）：Workers 不运行 git，所以只通过 `bash` 或在 Linux 容器中改动的文件不会列出，子代理会话也不记录。每个被编辑的文件会在本 turn 第一次编辑前和 turn 结束时各整体复制一份，存入工作区 VFS 的 `/.dsh/workspace-changes`，以内容的 SHA-1 命名，因此一个 turn 在内存中最多同时保留两份文件内容；与上游一致，超过 2 MiB 的文件列出但不计行数；与上游不同，任一侧超过 20,000 行的文件也一样，因为比较会按行分配内存（一百万个短行的峰值达 265 MiB 堆内存，超过 Durable Object 的 128 MiB）；含 NUL 字节的文件列为二进制，内容未变的重写不列出，只差结尾换行的文件会列出但没有变更行，一份摘要最多 500 个文件，超过 100 ms 的行比较按整份替换显示。摘要随常驻会话保留，与上游随 Session 保留一致；Durable Object 重启后，之前的 turn 不再有卡片，上一次激活留下的副本会在下一次捕获前删除。与上游一样，`dsh-agent-instructions` 在第一次请求前，通过工作区 VFS 从项目根目录（最近一个包含 `.git` 的上级目录，没有则为会话工作目录）到工作目录逐级加载 `AGENTS.md` 和 `CLAUDE.md`（以及它们的 `.local.md` 覆盖文件），合成一条不超过 64 KiB 的基线消息；`read`、`write` 或 `edit` 进入更深的目录时，下一次请求会加入该目录的说明。Workers 没有用户主目录，所以用户全局文件是 `/.dsh/AGENTS.md` 和共享 agent 根目录下的 `/.agents/AGENTS.md`（Edge 自己设置 `DSH_HOME` 和 `DSH_AGENTS_HOME`，因为该插件只从这两个变量读取主目录）。`working_directory` 工具（`dsh-tool-working-directory`）读取会话的工作目录，或把它切换到一个已存在的目录；切换是一条持久化的 `working-directory/change` 事件，早于本版本的发布无法读取它，降级后无法打开已写入该事件的会话，Edge 始终把 `/workspace` 视为已存在的目录。Skill 工具通过 `SkillRegistry` 提供。与上游 `dsh-skill-filesystem` 一致，skill 是带 `name` 和 `description` frontmatter 的 `<name>/SKILL.md` 或 `<name>.md` 文件，位于项目的 `.dsh/skills`、`.agents/skills`，以及用户目录 `/.dsh/skills`、`/.agents/skills`（Workers 没有用户主目录），通过工作区 VFS 读取，每个目录最多读取 100 个条目，每个 skill 文件不超过 64 KiB；新增或修改的 skill 从下一个 turn 起生效，变更后打开的会话的 `/` 菜单也会列出它；与上游一致，Web 客户端在页面刷新前保留已打开会话的 `/` 列表。这个菜单由上游的会话 skill 目录（`skills/list`）应答，并带有每个 skill 的文件路径；Workers 没有文件监听，因此 Edge 在列出前重新检查该会话的 skill 根目录，并为它提供工作区文件元数据，因为常驻会话的工作目录检查要在 turn 之外读取这些元数据。通过 owner 接口 `GET/PUT/DELETE /api/skills` 存储的 skill（`EdgeSkillProvider`，DO KV）优先级低于它们。MCP 使用 Edge 自己的客户端（`edge-mcp-client.ts`；未挂载上游 `dsh-mcp-client`）：仅支持 Streamable HTTP（Workers 上不可用 stdio），每次调用单独建立连接。probe 显示具备 `resources` 能力的服务器会注册到上游 `dsh-mcp-resources`；只要配置了一个这样的服务器，就会提供它的 `list_mcp_resources`、`list_mcp_resource_templates` 和 `read_mcp_resource` 工具（已有服务器需重新 probe 一次才会记录该能力）；不具备 tools 能力的服务器 probe 后工具目录为空。资源调用与该连接器的只读工具一样遵循其工具权限，只有设为每次调用前询问时才会先询问。每个资源响应体限制为 1 MiB，传输中一旦超过就失败，因为 SDK 会整体解析响应，而读取结果（包括二进制数据）会保存在会话日志中；不带 cursor 的列表最多跟随 10 页或 1 MiB 的条目，并返回停下处的 cursor。与上游一致，二进制内容只以描述的形式交给模型。在 Settings → DSH Edge → MCP Servers 中配置服务器；工具名格式为 `mcp__<serverName>__<tool>`。逐个针对 Worker-compatible capabilities 增加其余工具，不宣称不可用的 host 行为。 |
| Attachments | 本地 attachment storage、上游 image reference、composer、gallery、lightbox 与 provider conversion | 在原生 storage seam 上适配 | 原样复用上游 `AttachmentStore`、admission、协议、授权、UI 与 DeepSeek conversion。不可变的图片字节（PNG 或 JPEG；图片优化为保留透明而转换时为 WebP）按 SHA-256 identity 存入新部署与 pre-attachment 部署使用的 64 MiB、按 512 KiB 分块的 DO backend，或存入已固定使用私有 R2 的部署的 R2；session event 只保留上游 ref。每个 owner instance 首次选择的 backend 会被固定，认领或升级不会让既有引用失联。图片为 PNG 或 JPEG，每张最多 3.5 MiB，每条消息最多 4 张。其他文件经上游上传路由流式写入，按上游 `files/<prefix>/<digest>/<name>` 布局原样保存在工作区 VFS 的 `/.dsh/attachments/v1` 下，每个最多 64 MiB（上传必须声明长度，超限的在写入任何字节前即被拒绝），告知模型的就是这个路径；模型的文件工具与轻量 shell 可在此读取，但路由到 Linux Container 的命令看不到这些文件。 |
| Goal tracking | `GoalService` 提供 create/edit/pause/resume/complete/clear mutation 与 GoalBar UI | 复用 | 作为 cordis 插件安装上游 `GoalService` 和 `ToolGoal`（直接组合）。浏览器 GoalBar mutation 通过 `TypertGatewayService` 路由到 `/api/goals/<method>`。`SessionProjectionCache` 在 DO 重启后持久化 goal 状态。上游的 `/goal` 命令（`dsh-command-goal`）可以查看、设定、编辑、暂停、恢复或清除会话的目标。每一轮目标都在 Edge 的 turn 内运行，带有工作区、时限和实时事件：上游的轮次驱动排入下一轮期间，turn 保持打开；在 turn 之外启用目标的调用（`/goal`、GoalBar 的恢复）在一个新的 turn 内运行。 |
| Context compaction | Token 计量、自动压缩和 session 标题生成 | 复用 | 直接安装上游 cordis 插件。上下文压缩、token 计量、工具结果修剪和自动 session 标题生成原样运行。上游的图片卸载（`dsh-compaction-image-offload`）在请求中的图片超出路由预算（默认每个请求 600 张）时让 turn 继续：它用一条 `image/offload` 会话事件把最早的图片标记为已卸载，然后重试请求。 |
| Session projection | 向已连接客户端实时推送派生状态 | 复用并添加 Edge bridge | 上游 `onChanged` 回调向已连接 WebSocket 客户端推送 title、model、goal 和会话统计 projection frame。上游 `dsh-session-stats`（整个会话的 turn 数和 step 数，以及模型、工具和首 token 耗时）为 Web 聊天视图提供数据。`SessionProjectionCache` 在 DO KV 中缓存 projection，用于冷 session 恢复。 |
| Session history tools | `dsh-tool-session-query`，上游的可选包，不在任何上游预设中，基于 `ctx.sessionQuery` | 复用，默认关闭 | Settings → DSH Edge → 可选工具 → 会话历史检索会挂载上游的 `session_search`、`session_event_search`、`session_trace`、`session_event_trace` 和 `session_event_read`（设置命名空间 `edge-tools`，也可用 `GET/PUT /api/tool-settings`）；修改从下一次模型请求起生效。它们按会话工作目录判断访问权限，而 Edge 的会话都从 `/workspace` 开始，所以同一实例中的每个会话都能读取其他所有会话。`EdgeSessionQuery` 让这些读取保持有界：检索最多读取最新的 32 个会话，每个不超过 512 个事件和 256 KiB（更长的日志会被跳过，检索单个会话时只覆盖它的第一页）；标题来自会话摘要行；按 id 检查访问权限时只读取这些会话的头信息；`session_event_read` 以每页最多 4 MiB 的分页读取目标事件及其相邻事件，不加载整段日志；`session_event_trace` 拒绝超过 10,000 个事件或 8 Mi 已存字符的日志，更小的日志加载一次；`session_trace` 会列出已存会话（会话表没有父会话索引），会话超过 2,000 个的实例会被拒绝。对 `session_event_read`，已存行在选取内容前先检查大小，超大的行不会被加载（历史页等其他读取仍是每页一次查询）。开关为开但挂载失败时，会在下一个回合前重试。 |
| User questions | `UserQuestionService` answerer waterfall、`ask_user_question` 工具、`dsh-api-remotes` forwarded-event selection 与 Web question composer | 复用并添加 Edge connection seam | 直接安装上游 `UserQuestionService`、`dsh-tool-ask-user` 和 `dsh-api-remotes` Host entry，让模型调用 `ask_user_question` 时，其 Agent-scoped question 经 gateway 拥有的 `$events` stream 转发到浏览器 composer，答案经 `/api/$events/result` 作为 tool result 返回。gateway 会在 socket 断开时保留 pending question，并在重连后重新投递。Edge 只增加一个 `ctx.connection` seam，为 Durable Object 捕获 gateway 的 RPC interceptor，因为 Workers 没有上游 web server。 |
| Plan mode | `PlanModeController` 及其记录在日志中的 `plan` projection、`plan:policy` prompt section、`/plan` 命令、`exit_plan_mode` 工具与 Web plan control | 复用 | 直接安装上游 `dsh-plan-mode`，并提供部署自有的 `section`。`/plan`、`/plan <message>` 和 `/plan off` 经上游 command runtime 执行；plan projection 像其他 projection 一样到达浏览器；`exit_plan_mode` 通过与 `ask_user_question` 相同的 user-questions 路径把完成的计划交给用户审阅，批准后从下一个被接受的 step 起离开 plan mode。Web 端 `dsh-client-ui-plan` control 加入装配。Plan mode 只是引导而非强制：所有工具仍可调用。 |
| Queue and steer | 编辑、移除或提升排队消息；steering 运行中的 agent | 复用 | Queue mutation 以同步 live-inbox mutation 为接纳点。Steer 模式向运行中的 `ReactLoopAgent` 发送 next-step 消息。 |
| Authentication and tenancy | 本地 trusted-user boundary | 单 owner 适配 | 要求一个高熵 Worker secret，把它交换为带签名、有效期 30 天的 HttpOnly `SameSite=Strict` cookie，并把所有已接纳请求路由到一个固定 owner object。这里刻意不提供注册、用户数据库、角色或多租户路由。 |

浏览器请求路径是：

```text
Cloudflare static assets -> upstream Web shell + client plugin graph
  -> POST /api/session.create through the upstream HTTP carrier
  -> host/workspace-changed + session/subscribed over Durable Object WebSockets
  -> POST /api/session.prompt with the client rpcId
  -> 上游图片 admission 校验图片，并把不可变字节存入所选 R2 或 DO backend
  -> canonical session event 只保留上游 sha256 attachment ref
  -> AgentRegistry live lookup or resume
  -> sessionPersistence.prepare through PersistenceCoordinator on cold resume
  -> ReactLoopAgent.followup(queue) or ReactLoopAgent.steer(steer)
  -> pre-step admission gate waits for the sessions.flush durability barrier
  -> session/queue snapshots publish live and replay on mux reconnect
  -> TypertGatewayService dispatches goals/edit, goals/clear via Typert RPC
  -> onChanged bridge pushes title/model/goal projection frames
  -> SessionProjectionCache reads/writes KV for cold-session state
  -> upstream dsh-llm-deepseek plugin resolves configuration from settings + launch environment
  -> 上游 attachment resolver 读取并校验已授权的 backend 字节
  -> upstream LlmRuntime + ReactLoopAgent stream/event pipeline
  -> upstream ToolRuntime native bash, read/write/edit, or web_search call
  -> upstream WebRuntime + DeepSeek native search provider for web_search
  -> direct just-bash backend in the owner Durable Object
     (or optional Computer Worker Shell when LOADER is bound)
  -> Durable Object /workspace VFS
  -> upstream tool/result and next model step
  -> ReactLoopAgent appends canonical inbox, message, tool and boundary events (V3 embeds compact streams in assistant events)
  -> sessions.flush durable barrier -> Durable Object SQLite backend
  -> session/event, projection, and status frames over Durable Object WebSockets
  -> upstream Web runtime reconciles and renders the canonical events
```

本地集成套件使用 SSE stand-in，并运行真实 Wrangler、Durable Object SQLite、本地 R2、Direct Computer workspace backend、静态资源、HTTP carrier 和 WebSocket。Direct 模式覆盖 DO attachment storage，Isolated 模式覆盖私有 R2。它们共同验证：

- owner 登录、API/WebSocket cookie enforcement、旧 selector 拒绝和 Direct shell 禁止联网；
- 上游 session create/list/history/search/prompt/rename/fork 以及 queue edit/remove/steering 流程；
- 图片经上游 composer、协议、provider 完成准入，以及授权、fork 复用与重启持久性；
- Workspace create/list/rename/delete/reorder/archive、实时/重连 baseline 和 Host frame；
- 真实浏览器启动、UI 发起的 Workspace rename、图片 turn、内容搜索、branch、archive 和 session 过期后的登录恢复；
- 对话连续性、event 重放、两步 bash 与 Web Search tool 交互，以及 Wrangler 重启后恢复；
- Typert URL 路由验证和 snapshot 级别工具定义验证；goal 和文件工具的端到端集成测试尚未实现。

一项聚焦故障测试证明，入队后的持久化失败会阻止模型调用，不会把已唤醒 Agent 的 prompt 报告为拒绝。仓库中的 model-visible 与 ARIA golden 会固定 tool transcript 和组装后的上游 Web client。真实 DeepSeek 调用需要开发者的 key，故不纳入仓库测试套件。

## API key 边界

`DEEPSEEK_API_KEY` 可以在部署时作为 Worker secret 设置，也可以之后通过 Settings → Models 页面输入。Edge credential provider 先查 Durable Object KV，再降级到 Worker 环境变量；解析值保持请求级作用域，绝不写入 session event 或 response。首尾空白会被移除；空白值视为未配置。

其他服务商的 API key 也在同一个 Models 页面填写，按该服务商路由所指定的凭据名（例如 `OPENROUTER_API_KEY`）存入 Durable Object KV。这些 key 不从 Worker 变量读取：只有 `DEEPSEEK_API_KEY` 为了兼容 Models 页面出现之前的部署而保留这一兜底。路由的附加请求头以明文存在设置文档里，所以只接受不会携带密钥的请求头：`HTTP-Referer`、`Referer`、`X-Title`、`OpenAI-Organization`、`OpenAI-Project` 和 `anthropic-beta`。需要第二个认证头的网关（例如开启认证的 Cloudflare AI Gateway 所需的 `cf-aig-authorization`）暂时无法配置。路由的 key 名不能使用属于 DeepSeek 或其他 Edge 功能的名字（`DEEPSEEK_*`、`MCP_TOKEN_*`、`DSH_EDGE_*`），因此部署的 DeepSeek 密钥不会被发到其他地址。每个路由都必须有自己的 key 名：没有的话，pi-ai 会到进程环境里找 key，而 Workers 会把部署的 secret 暴露在那里。

| 变量 | 用途与校验 |
| --- | --- |
| `DEEPSEEK_BASE_URL` | Anthropic-compatible Messages endpoint。默认为 `https://api.deepseek.com/anthropic`。DeepSeek 以前的 OpenAI-compatible 根地址（`https://api.deepseek.com`、`/v1` 或 `/beta`），无论设在这里还是保存为 Models 页的 Base URL，启动时都会改成该默认值；其他地址返回 404 时，保留原报错并补充提示：该地址可能不支持 Messages API，以及在哪里修改。必须是不含 URL userinfo 的 HTTP(S) URL。Browser 投影会省略可能携带 gateway credential 的 query 与 fragment。 |
| `DEEPSEEK_SEARCH_BASE_URL` | Native Web Search 的 Anthropic-compatible Messages endpoint。默认为 `https://api.deepseek.com/anthropic/v1`；必须是不含 userinfo、query 与 fragment 的 HTTP(S) URL。Search 不跟随 redirect。 |
| `DEEPSEEK_MODEL` | 已校验的部署默认模型；默认 `deepseek-flash`（DeepSeek-V4.1-Flash，支持图片输入）。每个 session 可选择其他上游 catalog 条目。与上游一致，在任一 session 中最近一次选择的模型会成为新 session 的起始模型，因此该值只在所有者第一次选择模型之前生效。 |
| `DEEPSEEK_REASONING_EFFORT` | `off`、`low`、`high` 或 `max`；默认 `high`。 |
| `DEEPSEEK_MAX_OUTPUT_TOKENS` | 可选正安全整数，用于覆盖默认的 256,000-token chat 上限。 |
| `DEEPSEEK_STREAM_IDLE_TIMEOUT_MS` | 可选正整数，上限 2,147,483,647；默认 120,000 ms。 |
| `DSH_EDGE_DEFAULT_COMMAND_TIMEOUT_MS` | Computer 命令默认 timeout；默认 120,000 ms。 |
| `DSH_EDGE_MAX_COMMAND_TIMEOUT_MS` | 调用方可选 timeout 上限；默认 120,000 ms，且不能低于默认 timeout。 |

部署配置无效时，会在查询 session 或创建 SSE response 前失败。Edge 挂载上游 `web_search` 与 `web_fetch`，使用 30 秒 tool-call timeout 和结构化 result。`web_fetch` 使用上游匿名 HTTP(S) provider，并在 Worker 内把 HTML 转成 Markdown。它只接受不含嵌入凭据的 HTTP(S) URL，在每次请求和每个同源 redirect hop 前拒绝 IP literal 与本地命名范围，并限制 URL 长度、response 字节数、解码字符数、redirect 次数与耗时。Worker 启用 Cloudflare 的 strictly-public global fetch 路径，DNS 变化无法把模型选择的 URL 路由到同区 origin 或私网 binding；需要 JavaScript 渲染的页面不属于这条基础 HTTP fetch 路径。

### Owner 认证

- `DSH_EDGE_ACCESS_KEY` 是 single-owner 边界。它必须包含 32–512 个 UTF-8 字节，不得带首尾空白或控制字符；应生成随机值，而不是复用人工密码。
- 登录会创建带签名、有效期 30 天的 HttpOnly `SameSite=Strict` cookie。HTTPS 使用仅限当前 host 的 `__Host-dsh_edge_owner` 名称并带 `Secure`；本地 HTTP 使用无前缀 cookie。
- Cookie 不包含用户数据，不会转发给 Durable Object，并在 access key 轮换后失效。
- 未认证的 API 与 WebSocket 请求返回 401。只有携带 `WWW-Authenticate: DshEdgeOwner` 的 owner-authentication 401 才会让同 origin shell 导航到 `/login`；provider/配置的 401 诊断仍然可见。
- 来自其他 origin 的已认证浏览器 API 与 WebSocket 请求，即使携带 same-site cookie 也会返回 403。
- Web shell 会把浏览器传输层声明为拥有宿主（`__DSH_TRANSPORT__.ownsHost`），因此上游仅限 loopback 的信任层级（它决定宿主持久化的插件设置和设置文档编辑器是否可用）对任意 origin 上的已认证 owner 同样生效。
- Asset policy 会阻止通过 `/`、`/index.html` 或 SPA fallback 进行 frame 嵌入。`/` 重定向到 `/login`；`/api/health` 与 immutable asset 保持公开。

这里刻意不是 account system 或多租户边界。

## 安装到 Cloudflare

| 目标 | Cloudflare 要求 | 命令边界 | Health identifier |
| --- | --- | --- | --- |
| Direct（默认顶层） | Workers Free；无 Loader binding | 在 agent/VFS Durable Object 中运行加固 just-bash，带明确 timeout、有界输出/环境，并禁止网络命令 | `just-bash-direct` |
| `env.isolated` | Workers Paid 与 `LOADER` | 在独立 Dynamic Worker 中运行 Computer Worker Shell | `just-bash-isolated` |
| `env.container` | Workers Paid、`LOADER` 与一个 `basic` Container | 多数命令在 isolated Worker Shell 中运行；需要 Linux 的命令经分流通过 computerd 进入 Debian 容器（见 `src/bash-routing.ts`）；使用 isolated Worker 产物 | `linux-container` |

Direct 模式比独立 Worker 的隔离更轻；不要把 single-owner 部署暴露给不受信任的用户。Workers Paid 是每月 5 美元起的 Workers 订阅，并非 Cloudflare Pro 网站套餐。Worker 名称拥有独立的 Durable Object storage 与 secret；两种模式同时在线时请使用不同名称。

`wrangler.jsonc` 仍是两种模式的 canonical source：

- 发布打包为每种模式构建一个经过测试、已 minify 的 artifact。
- Direct 只替换 Computer 中不可达的 Dynamic Worker shell-core module；Workspace adapter 与 command export 仍使用上游实现。
- Isolated 保留该 shell core，并把不可达的 Direct backend 替换成 fail-closed module。因此每个 artifact 只携带所选 command runtime。
- 安装器生成私有 mode-specific config，指向所选 artifact，并通过 `no_bundle` 上传。用户机器不会重新构建 dsh-edge，也不会把 Harness package 解析进新 Worker。
- Container 模式部署 isolated 产物，并使用 `docker.io/pawaca/dsh-edge-computer:<version>` 镜像。该镜像由发版流程从 `container/Dockerfile` 构建，每个版本在 npm 发布前推送一次。安装时不需要本地 Docker；`pnpm dev:container` 和 `pnpm test:container` 会在本地构建 Dockerfile，因此需要。
- CI 从已安装 tarball 启动 Direct artifact。打包时会拒绝超过 Cloudflare 未压缩 64 MiB 上限的 Worker（Cloudflare 不限制压缩后体积，临时账户也一样），CI 还会对照 Cloudflare 1 秒的启动上限检查每个产物的启动耗时。

### 安装与升级

无需克隆仓库，即可运行稳定版安装器：

```sh
npx dsh-edge install
```

该命令通过 npm `latest` 渠道解析。只有在存在更新的预发布版且你想主动试用时，才使用 `npx dsh-edge@next install`。

每个 prompt 都直接按 Enter，就能装好一个可用实例并在浏览器中打开。用相同 Worker 名称再次运行同一条命令，会原地更新该实例。`upgrade` 行为相同，只是名称尚不存在时会拒绝：

```sh
npx dsh-edge upgrade
```

如果当前安装版本包含 `-alpha` 或 `-rc`，请执行一次 `npx dsh-edge@latest upgrade` 切换到稳定渠道。Edge 设置页会根据已安装版本推导命令；如果不明确使用这条 `@latest` 命令，现有预发布部署会继续跟随 `next`。

### 安装器会问什么

安装器按以下顺序提问：账户、Worker 名称、要改什么。所有问题都在唯一一次确认之前问完；确认之后只剩部署和交接。

- **账户。** 已登录的 Cloudflare 账户排在最前，按 Enter 即得到永久实例。未登录时临时账户排在最前：无需登录，只运行免费能力，必须在 60 分钟内认领。也可以登录或注册账户。
- **Worker 名称。** 安装器检查该名称在账户中是否已存在。只用 `--name` 就能选中 Worker，与部署它时的能力无关。
- **已存在的名称就是更新。** 安装器从 Worker 的 binding 读出它当前能做什么，提供 **Update it**（默认）、**Update and change what it can do**、**Update and reset the access key**、**Use another name** 和 **Cancel**。选 "Update it" 即是确认：以相同能力部署新版本，并保留对话、文件、owner access key 与 DeepSeek key。安装器不会替你改名，也不会再次询问 secret。
- **新名称会询问 agent 要做什么。** 选项逐级包含，价格放在第二位：
  - `Research and write`（direct 模式，默认）：联网搜索、读网页、写文档，并通过 MCP 连接你的工具。可在 Workers Free 上运行。
  - `+ Analyze data and split big jobs`（isolated 模式）：增加 `workflow`，把任务交给并行子 agent，以及 `ralph`，用全新子 agent 循环推进；在以 PTC 模式 agent 预设开始的会话中增加 `run_code`，用脚本处理你的数据（与上游一样，该预设不提供 `workflow`，也不提供 `ralph`）。需要 Workers Paid（每月 5 美元起）。
  - `+ Work on code projects`（container 模式）：增加用于 git、npm、python 的 Linux 容器。命令先在隔离 shell 中执行；只有当命令启动的程序不在轻量 shell 已验证的清单内、命令无法被可靠解析，或 agent 设置了 `linux: true` 时，才分流到容器。容器内最多同时运行两条命令。需要 Workers Paid，外加容器运行时长。
- **调整能力。** 能力列表会标出当前选择。选择更少的能力时会二次确认，列出将被移除的内容，默认 No。离开 container 模式后，安装器会保留该 Worker 的 Container application 作为回退目标，并打印删除它的命令；登录确认新版本正常后运行该命令，即可停止 Container 计费。
- **唯一一次确认。** 摘要列出实例能做什么、费用、账户、Worker、图片存储以及下面的默认值。使用临时账户时，确认即同时接受 Cloudflare 服务条款与隐私政策。

### 默认值

- **图片** 存入实例的 Durable Object（64 MiB），因此新安装无需配置 R2。每个部署都会记录 attachment-storage marker，更新时保留 marker 或 binding 指定的 backend。已使用私有 R2 的 Worker 会继续使用 R2；如果 R2 不再启用（Cloudflare 错误 `10042`），安装器提供账户专属的启用链接、重试或取消，绝不切换到会让图片引用失联的 backend。图片功能之前的 Worker 没有可失联的引用，会改用 Durable Object 默认值。Active rollout 混用 backend 时会拒绝猜测。
- **Owner access key** 在新实例上自动生成，并在安装完成时显示。如需自定义，请在安装器环境中设置 `DSH_EDGE_ACCESS_KEY`（32–512 个 UTF-8 字节）；它不会进入 shell 历史或进程列表。普通更新会保留现有 key。Cloudflare 无法再次显示已存的 key，所以丢失时请按部署所在的发布渠道运行升级命令，并选择 **Update and reset the access key**：稳定版运行 `npx dsh-edge upgrade`；已安装版本包含 `-alpha` 或 `-rc` 时运行 `npx dsh-edge@next upgrade`（登录页会显示适用于该部署的命令）。安装器会用新 key（自动生成，或取自 `DSH_EDGE_ACCESS_KEY`）部署该版本，并只显示一次。对话、文件与 DeepSeek key 均保留；旧 key 随即失效，所有已登录的浏览器都需要用新 key 重新登录。
- **DeepSeek API key** 在安装完成后于 Settings → Models 中添加。安装器不会询问它。
- **图片优化**（Cloudflare Images binding）对你自己的账户开启，对临时账户关闭。开启后，超过上游 2048 × 2048 像素预算或 4 MiB 的图片、WebP 或 GIF、以及带旋转方向的 JPEG，会按上游的方式规范化（缩小、转正、取第一帧、转为 JPEG 或 WebP）；已经符合要求的 PNG 或 JPEG 按原样保存，不产生费用。Cloudflare 每个月对同一张图的处理只计一次：每月前 5,000 次免费，之后免费计划会拒绝新的处理，付费计划按量计费。Images 拒绝或失败时，未超出无 Images 时限制的图片按原样保存，更大的图片会被拒绝并说明原因。Worker 自己从不解码这类图片：一张 8,192 像素的位图放不进它的内存。

### Credential 交接与激活

- 新实例的 owner key，或重置 key 的更新所生成的新 key，通过权限模式为 `0600` 的临时 secret 文件传递，命令结束后删除该文件。Wrangler 以增量方式应用该文件，Worker 的其他 secret 保持不变。普通更新不传 secret 文件，Cloudflare 会保留 Worker 现有的 secret。Wrangler 只收到 allowlist 内的 runtime 环境与当前选中的 Cloudflare authentication；其他 ambient secret 与 Node 注入选项不会进入子进程。
- 部署 URL 来自 Wrangler 的结构化输出。添加 `--verbose` 可查看完整部署诊断。
- 上传后，安装器会在不携带 credential、不跟随重定向的前提下，最多观察公开 `/api/health` 45 秒。只有精确 package 版本与能力才算数；propagation、challenge、占位页、传输错误与旧 release response 均保持 pending。随后新实例或重置 key 的更新会用新 key 登录并检查 `/api/ready`。普通更新不知道被保留的 key，因此会把已验证的版本报告为 live，并请你登录确认。
- 观察到期仍以成功退出，并请 owner 稍后刷新。该观察不调用 DeepSeek，也不触碰 Durable Object 状态。
- 最终卡片输出 URL、新的 owner key（普通更新后显示 "unchanged"）与下一步，然后询问是否在浏览器中打开该 URL（默认 Yes，仅限交互式终端）。临时账户还会收到必须在 60 分钟内认领的 bearer claim URL。
- 上传被拒绝时会明确报告未安装。Wrangler 如果已创建临时账户，仍显示其 claim URL，但不把未使用的 owner key 显示为 active。付费能力被 Cloudflare 拒绝时，会建议启用 Workers Paid 或改选 `Research and write`。
- 上传成功但交接失败时，恢复卡片会在命令按失败退出前输出已生效 owner key 与所有已知 URL。
- 安装直接通过 Wrangler 上传；不会创建或绑定 GitHub 仓库、Cloudflare Builds 项目或源码构建流水线。

从 checkout 开发的贡献者可以用 `pnpm --filter dsh-edge bundle:direct` 和 `pnpm --filter dsh-edge bundle:isolated` 在本地复现两种 release artifact。两条命令都会检查 Cloudflare 未压缩 64 MiB 的 Worker 体积上限（Cloudflare 不限制压缩后体积），`pnpm --filter dsh-edge check:startup` 会对照 Cloudflare 1 秒的启动上限分析已提升产物的启动耗时。

贡献者可以在没有 key 且不发起网络请求的情况下重放完整的 Free 临时账户流程。这个 example 会运行实际交付的 bin、真实 prompt、Wrangler 子进程、结构化部署输出解析、公开激活观察与最终交接，并替换外部 Cloudflare 边界：

```sh
pnpm --filter dsh-edge example:install
```

## Edge API

### 上游 RPC carrier

- `POST /api/<upstream-method>` 接受受支持 `ApiProxy` 方法的上游 `ClientRequest` envelope。
- Web client 使用 session list/search/create/history/models/select/prompt/updateQueue/rename/fork/cancel；host description；Workspace list/create/rename/delete/reorder/archive；skills；agent presets；settings 与 credential description；以及 LLM catalog。
- `agentPreset.read` 通过上游只读 viewer 渲染程序化 Edge composition。`credentials.describe` 返回不含 value 的 credential state。
- Search 投影 canonical current-message surface，并返回有界的上游 result value。
- Fork 与上游 session controller 一致：`atSeq` 是包含在内的精确切点（不存在的事件会被拒绝），省略时选最近的完整回合前缀，并由上游的 `buildForkSeed` 标记切点、把未结束的回合以 forked 收尾；fork 保留 parent lineage。对已存的源会话，Edge 只读到切点为止，并拒绝超过 8,192 个事件或 8 MiB 的 seed，不会物化无界 history。`allowMigration` 会被接受；Edge 在 Durable Object 启动时已迁移所有已存会话，因此 fork 不会遇到仍需迁移的会话。
- 改名通过上游的 `sessionTitle.rename` 进行，它会同时停止仍在运行的自动标题生成，因此晚到的自动标题不会覆盖用户设置的标题。
- Queue mutation 通过 live upstream Agent inbox 编辑、移除或提升条目。同步 mutation 是接纳点；后续 write-behind 与 retirement retry 由 persistence coordinator 负责。
- Workspace mutation 通过 Durable Object backend 持久化上游 workspace-domain global 与 record shape。Archive 保留 session log 与 Workspace slot；unary response 与 Host frame 携带和上游一致的完整 snapshot。
- Goal mutation（create、edit、pause、resume、complete、clear）通过 `TypertGatewayService` 路由到 `/api/goals/<method>`。每个 `@Remote` Service 需要通过 `ctx.typert.register()` 注册其 Typert 描述符；不需要编写 per-method HTTP handler。

### 认证与 downlink

- `GET /login` 渲染 owner form。`POST /api/auth/login` 用已配置 key 换取 signed cookie；`GET /api/auth/session` 报告有效性；`POST /api/auth/logout` 清除 cookie。
- `GET /api/events.mux` 和 `GET /api/events.host` 升级为上游 downlink WebSocket。Durable Object 会把每个 socket 的 channel 与已验证 owner-session 过期时间序列化为 hibernation attachment，通过 alarm 在过期时关闭，并从 SQL 重建 canonical session 与 retained blank header。
- 每次 inbox splice 提交后，mux 都发布完整的 `session/queue` snapshot。客户端重连时会收到待处理的 live-inbox baseline。
- `POST /api/commands/list` 使用上游 generated-Remote envelope 返回空 catalog，因为 Edge preset 没有注册 human command。
- `GET /api/health` 返回公开 release/mode identifier 与配置的 attachment 默认值（`private-r2` 或 `temporary-do`）。它先验证 owner authentication、部署级 DeepSeek 凭据、模型/传输选择与命令 timeout，再报告 ready。
- `GET /api/ready` 要求 owner cookie，等待会话迁移、工作区注册、浏览器所需的会话/工作区控制器就绪，以及 DeepSeek 模型适配器完成注册。此检查不会向模型服务发起请求。启动失败时返回 HTTP 503 和 `runtime-initialization-failed`；公开 health 响应不能单独证明运行时可用。普通认证路由只等待基础初始化，不要求模型适配器可用；仅模型初始化失败时，仍保留历史记录和恢复访问。
- Health 不调用 provider、Durable Object、R2、VFS 或 shell。认证后的 agent-preset projection 会报告固定 backend、临时存储上限、部署默认模型，以及 runtime 实际读取的上游 catalog 与 session 选择范围。

Release 升级会自动兼容已知的旧版 Edge 取消记录，并在一个 SQLite 事务中迁移所有旧格式会话日志、摘要和空会话头。首次迁移写入前会解码并校验所有旧日志，避免后面的不兼容日志导致每次重启都重复消耗前面会话的迁移写入。预检会释放每份解码结果；迁移阶段在同一同步事务内再次解码，避免把所有历史同时保留在内存中。任一会话不兼容或写入失败时，本次启动的迁移会整体回滚。上游的格式还原一次转换整份日志，内存占用约为事件数据的四倍，因此事件数据超过 8 MiB 的旧格式会话会保持原样（Worker 日志中记录警告），而不是让每次启动都失败；实例照常启动，Edge 把该会话视为不存在：它不出现在会话列表中，按 id 读取（历史、摘要、`GET /api/sessions/:sessionId`）也会回答不存在（#371）。它的会话头和事件原样保留在 Durable Object 中，不会删除任何会话。此事务覆盖会话格式迁移，不涵盖任意插件存储改动或外部副作用。安装器会把上传输出中的 Cloudflare Worker 版本 ID 与公开 health 和经过认证的就绪响应分别比对，同一软件包的旧部署不能满足该检查；两种模式都包含版本元数据绑定。安装器检查运行时就绪状态，通过后才报告成功；超时会报告尚未验证，确认启动失败则报告错误和恢复信息。它不会在存储格式升级后自动回滚 Worker 代码。发布测试使用含旧取消记录的数据启动两种预构建模式，验证继续聊天，并确认不兼容数据不会误报就绪；打包后的 npm 产物也会验证经过认证的运行时就绪状态。

SQLite 的 `DELETE` 操作也会消耗每日行写入额度。迁移格式时，Edge 会比较旧格式与当前格式的事件行数：单条利用索引的探测查询在确认复制成本更高时即停止；如果复制当前事件的成本更低，就把迁移后的压缩日志写入替代表，并在同一事务内交换表，避免为每个旧 token 片段支付逐行删除费用；否则只替换需要迁移的会话。已有当前格式日志会保留，迁移失败也会回滚表交换。这会降低迁移写入量，但不能保证所有数据库都满足 Free 计划每日 100,000 行写入额度；数据库足够大或当天已使用较多额度时，仍可能触及上限。时间点恢复恢复的是数据，不会恢复账户已经消耗的每日额度。

### 诊断 REST 路由

- `PUT /api/workspace/file?path=/workspace/...` 写入 UTF-8 文件。
- `GET /api/workspace/file?path=/workspace/...` 读取 UTF-8 文件。
- `DELETE /api/workspace/file?path=/workspace/...` 删除文件。
- `POST /api/workspace/exec` 接受 `{ "command": "...", "cwd": "/workspace/..." }`，`cwd` 默认为 `/workspace`；每次执行都会使用默认 timeout（Shell 卡片保存的值，未保存时为 `DSH_EDGE_DEFAULT_COMMAND_TIMEOUT_MS`，且不超过 `DSH_EDGE_MAX_COMMAND_TIMEOUT_MS`），`timedOut` 会报告该 deadline 是否已过，`outputTruncated` 报告是否有输出未内联；`stdoutTruncated`/`stderrTruncated` 指出哪个流只显示了尾部，`stdoutSpillPath`/`stderrSpillPath` 给出完整输出的保存位置。
- `POST /api/sessions` 使用必填 `title` 创建持久 session；API 返回前，标题会写成标准、用户来源的 `session/title` 事件。如果 session 已持久化但 Workspace attachment 失败，500 响应会使用 `session/workspace-attach-failed` 并携带完整的已创建 `session`，调用方可以恢复其 id，而不会创建重复 session。
- `GET /api/sessions?after=...&limit=...` 列出一个有界 summary page；`GET /api/sessions/:sessionId` 读取一个 session。上游 persistence service 没有定义破坏性删除，因此这里不暴露 session deletion。
- `POST /api/sessions/:sessionId/turn` 接受 `{ "message": "..." }` 并流式返回持久 SSE events。
- `GET /api/sessions/:sessionId/events?after=...&limit=...` 重放一个有界 event page，并返回 continuation headers。
- `POST /api/sessions/:sessionId/cancel` 终止当前 Durable Object 进程持有的 active turn。

### Session 与 Workspace 行为

- Session 创建与 fork 在发布成功但 Workspace attachment 失败时返回 `session/workspace-attach-failed`，并携带已发布的 session 与 Workspace id。诊断路由返回相同 code 及完整的已创建 session。
- Prompt 和 queue-edit 文本共用 64 KiB 语义上限。10 MiB RPC 载体为 7 MiB 原始图片在 base64 与 envelope 膨胀后留出空间。
- Edge 没有目录流 provider，因此上游浏览器会在仅剩一个 Workspace 时隐藏 Delete，并在仍有恢复路径时重新显示。
- `@file` 补全通过 Computer VFS 为上游 `fileReferences/list` Remote 提供服务，行为与上游本地 provider（`dsh-file-reference-local`）一致：带 `/` 的查询列出那个目录，可以相对会话工作目录，也可以是工作目录内的绝对路径；裸片段在工作目录的递归索引上模糊排序（依次是完全相同、前缀、包含的文件名，然后是包含于路径和按顺序的子序列，因此 `rdm` 能找到 `docs/README.md`）。索引跳过上游排除的目录（`.git`、`node_modules`、`dist`、`build` 以及其他依赖和构建输出目录），最多保存 10,000 个条目、共 1,000,000 个路径字符，而上游是 50,000 个条目，因为每次列目录都会读取 Durable Object 的行：读取索引大约每个条目 3.5 行。同一工作目录的会话共用一份索引，最多保留 4 个工作目录的索引，最久未查询的先被丢弃。只有第一次裸查询需要等待索引；与上游一样，任一会话的工具返回结果后（各会话共用一个工作区）索引变为过期，下一次裸查询先用它回答，同时读取替换的索引。与上游的索引一样，工具之外的文件变更（属主 API 写入、上传）要到下一次工具返回结果后才会进入索引；带 `/` 的查询始终实时读取目录。`@session` 提及及其 pre-step 快照原样来自上游 session-reference 服务；它的候选发现以及任一 agent 作用域 Remote 都会通过上游 controller 恢复冷 session，并使其常驻直到 Durable Object 被驱逐。

### 限制与请求准入

| 表面 | 上限 |
| --- | --- |
| UTF-8 文本文件 | 1 MiB |
| Shell 命令 | 16 KiB |
| 用户消息或 queue-edit 文本 | 64 KiB |
| 每个流内联保留的 shell 输出 | 默认 64,000 字节（Shell 卡片的输出上限，1 KiB 到 1 MiB）；其余部分转存，每个流最多 16 MiB |
| Session-create JSON body | 8 KiB |
| Workspace-exec JSON body | 128 KiB |
| 承载消息的 turn 或 queue-update RPC | 10 MiB |
| 图片，开启图片优化（Cloudflare Images binding）时 | PNG、JPEG、WebP 和 GIF（取第一帧）；每条消息 8 张；每张及合计 7 MiB，即 10 MiB 消息体经 base64 后能容纳的大小；6,400 万像素；单边 8,192 px；存储时缩到上游的 2048 × 2048 像素预算和 4 MiB 以内 |
| 图片，未开启时 | PNG/JPEG；每条消息 4 张；每张 3.5 MiB；合计 7 MiB；4,000 万像素；单边 2,000 px |

- Request body 会增量消费。一旦越过路由上限，后续 chunk 只会排空而不会保留，路由返回 413。文件上传也会拒绝非法 UTF-8。
- 读取文件时先检查 VFS metadata，再通过相同的 1 MiB 上限收集已打开的 byte stream，从而关闭 `stat()` 与 `readFile()` 之间的增长竞态，且不保留无界值。
- 写入前，图片准入会完整解码声明的 raster 格式。
- 与上游 `bash-local` 一致，输出超过上限后命令会继续运行：每个流内联保留最后 64,000 字节（默认值，即 Shell 卡片的输出上限，可设为 1 KiB 到 1 MiB），整个流保存到 `/workspace/.spill/bash-<execution>.<stream>.txt`（与其他 spill 一样 30 天后删除），每个流最多 16 MiB（上游允许 64 MiB；每 512 KiB 约写 5 行 Durable Object 数据，已删除文件的字节在其内容闲置一天后由每日清扫释放，#358），超过后文件只保留开头部分，结果中会注明。内存只占尾部加每个流 1 MiB 的写入队列：写 spill 时会暂停读取命令输出。未超过上限的输出不写任何文件。轻量 shell（just-bash）一次性返回命令的完整输出而不是流式输出，所以输出超过 8 MiB 时仍会停止命令（exit 126，显示为 `[stopped: …]`）；这块缓冲位于它所在的 isolate 中，Direct 模式下就是 Durable Object。bash 工具按上游格式呈现结果：stdout、`[stderr]` 段、每个流的 `[output truncated; full output: <path>]`、`[timed out after Nms]`，最后是 `[exit code: N]`；终端卡片会显示工作目录。在 Linux Container 中，命令在两个输出管道都关闭后结束；如果用 `&` 启动的后台任务仍占着管道，则在 shell 退出 250 毫秒后结束，该任务之后写出的输出会被丢弃，容器继续运行（computerd 0.7.0，#373）。超过部署上限的 `timeoutMs` 会像上游一样被降到上限；HTTP exec 路由仍会拒绝它。`cancelled` 反映 adapter 请求的中断；`timedOut` 独立记录 deadline expiry。

### 持久性与并发

- 首次 session 持久化失败时，会先丢弃保留但尚未物化的 batch，再 dispose 新上游 agent handle，因此 teardown 无法提交一个创建请求已经报错的 session。
- Lazy blank session 只保留上游 header；首个 canonical event 会在同一 SQL transaction 中删除该 header。
- 每个 turn 持有一个上游 handle，并在 stream 完成后 dispose，避免曾访问的对话在 Durable Object 整个生命周期内常驻。
- 部署 settings 会在声明进程内 owner 前解析。一个 owner 进程会拒绝并发 turn；cancel 使用原生 agent path。
- Prompt 只有在 inbox event 跨过 `SessionStore.flush()` 后才会被接纳并发布 running state。Streamed event 也会先跨过同一 durability barrier，再通过 WebSocket 或 SSE 发送。
- Queue edit、remove 与 steering promotion 以同步 live-inbox mutation 作为接纳点。后续 write-behind 与 retirement retry 由 `PersistenceCoordinator` 负责，后来的存储尝试不能反转已接纳的 mutation。
- Session rename 对 active 与 cold session 都以同步 title append 作为接纳点。
- Workspace global state 与 record 在 Edge-specific physical key 下保留上游逻辑 schema。DO transaction 原子组合 record 与 registry-order 变更；进程内 chain 串行化 Workspace mutation。
- 已提交的 rename、delete、recreate、session reorder、attachment 和 archive 变更会发布对应上游 Host frame。`workspace.list` 会在重启后恢复完整 baseline。
- 冷恢复使用上游 interrupted-turn repair 关闭开放的持久 event tail；canonical `session/end-seed` marker 保留生命周期边界。

### 有界读取与 canonical history

- Replay 会独立确认 session 是否不存在，因此 persistence corruption 与 SQL failure 不会折叠成 404。它只读取一个有界 SQL page，并限制编码后的 response。
- `PersistenceCoordinator.readValidatedPage()` 对每个 page 执行 identity、format、legacy shape 和 event vocabulary 校验，不把 Edge pagination 加入公共 persistence service。
- Legacy normalization 如果需要更早的 message，只会通过同一受字节约束的 loader 重读一个 prefix；必要 prefix 无法装入预算时会拒绝该 page。
- 冷浏览器 history 在 SQL 中选择消息边界，并仅在固定事件数与存储字节上限内加载所得连续区间。
- Session listing 查询一个有界 canonical header/title summary page。Detail 读取 canonical point summary 或 retained blank header；turn existence check 使用 point query，不投影完整 log。
- 实际 model、system prompt、adapter defaults 和 tools 仍使用标准 `request/header` 事件。Request-scoped adapter 应用已校验的部署级 reasoning 与输出策略。
- Workspace 路径必须位于 `/workspace/` 下。
