## 4. 技术设计（Technical Spec）

> 本版技术设计基于最新产品设计重写：
>
> - 身份模型使用「human: `targetId=member.id + displayName`，agent: `targetId=agents.name + principalId=agents.id + hidden member_id + displayName=members.display_name`」，其中默认 human-visible Web UI 统一显示 canonical display label；detail / inspect surface 可展示带标签的 id metadata；`users.name` 只保留为 legacy alias
> - 产品模型按 `Role / Agent / Instance` 理解：Role 是可版本化资产，Agent 是 workspace actor，Instance 是 machine-bound runtime；Instance 的临时状态由 daemon runtime 管理
> - 消息系统按 **IM + append-only thread log** 来建模；严格受限的 rewind 通过服务端 `rewound` lifecycle tombstone/scrub 让 target+suffix 从普通读路径消失，而不是裁剪主 transcript 行
> - 消息存储采用 **TiDB 热数据 + 冷历史归档（Archive Storage：filesystem / S3）** 的双层结构，目标是明显降低数据库长期负载与成本
> - daemon 一方实现与外部 daemon 差异分析边界统一由 `specs/daemon/daemon.md` 约束

### 4.1 架构总览（单体优先，围绕 append-only log 简化）

单个 Go 服务（`agentteam-server`）同时提供：

- **HTTP API（public）**：给 Web / CLI 使用，负责 workspace / member / channel / dm / machine / agent 管理
- **HTTP API（internal）**：给 agent runtime（经本机 MCP chat-bridge）使用：`/api/v1/workspaces/:workspaceId/agents/:agentId/runtime/*`
- **WebSocket（control plane）**：给 machine daemon 使用：`/api/v1/workspaces/:workspaceId/daemon/connect?key=...`
- 冷历史对象和读取兼容能力继续由 Archive Storage（filesystem / S3）提供；消息写入侧当前不运行冷归档 worker

#### 4.1.1 当前实现代码位置索引（As-Is）

> 目的：把本文“技术设计”与仓库真实落地点绑定在一起，避免后续演进导致文档漂移。

- 进程入口：`cmd/agentteam-server/main.go` → `internal/app/app.go`
- 显式数据库 schema 初始化入口：`cmd/agentteam-schema/main.go`
- 配置与环境变量：`internal/config/config.go`
- HTTP 路由装配 + 健康检查：`internal/api/router/router.go`
- Public HTTP API（`/api/*`）：`internal/appapi`（路由：`internal/appapi/handlers.go`；handlers：`internal/appapi/handlers_*.go`）
- Owner-private agent context uses parent channel/thread/task targets plus authenticated server-side context. User-facing entry points are channel private-agent collaboration and Task execution surfaces.
- Socket.IO realtime（`/socket.io/*`）：`internal/appws`（路由：`internal/appws/routes.go`；rooms：`internal/appws/rooms.go`）
- Internal Agent API（workspace-scoped Agent Runtime，daemon/chat-bridge）：`internal/api/agent`（路由：`internal/api/agent/routes.go` + `internal/api/agent/api_*.go`）
- Workflow canonical object chain：`Plan → Binding → Invocation → Run`；HTTP ownership 位于 `internal/api/workflow/{plan,binding,invocation,run}`，持久层位于 `internal/store/workflow/{plan,binding,invocation,run}`，领域与执行器位于 `internal/workflow/{plan,binding,invocation,run,orchestrator}`，共享 Web Builder/运行页位于 `web/src/agentteam/workflow`。Web collection surface 由 `WorkflowHubLayout` 持有固定 Header 和 Overview/Workflows/Schedules/Runs Router Tabs，四个一级集合页同步提供真实 Filter Bar，路由或数据 Loading 只替换下方 Body；一级 Workflow rail 不再拥有二级 sidebar。`GET /api/v1/workspaces/:workspaceId/workflow/calendar?from&to` 由 `internal/api/workflow/run`、`internal/workflow/calendar` 与 `internal/store/workflow/calendar` 实现：在一个只读一致性快照内读取窗口 Run 和 active Schedule cursor，返回真实 Run 与未接受的 Schedule occurrence；31 天/5000 项预算超限时显式拒绝而不截断。Web Overview 直接读取该完整窗口，按 Invocation occurrence 排列 Run 与未来 Schedule occurrence；“仅我的”范围遍历 Plan 游标并按 `createdBy.memberId` 解析，不继承 Run/Plan 单页上限。旧 Workflow document/API/worker/runtime、task-event activation 和旧 ledger tables 已退出产品边界，不保留 fallback；完整契约见 `specs/workflow/agentteam-workflow-platform.md`。Run 冻结 `workflow_run_instances.team_id`（varchar(255)）来自服务端唯一受权来源：Team-linked Plan 的 approved+activated+非删除 `workspace_team_workflows` relation，或 unlinked/dynamic `invoke` 的显式、服务端校验调用者成员身份的 `teamId`（linked relation 冲突即拒绝）；不可从可见文本/callback 伪造。成员上下文在消息写事务内、任何持久化/fan-out/wake 副作用前对 responder 授权并原子写入 metadata/outbox receipt（零副作用 fail-closed），callback/retry/recovery/replay/fan-out 复用同一 frozen snapshot；最小 per-responder 上下文（team identity/purpose、`teamMemberId`、role label、role overlay，空 roster，不含 wake/分工）经 delivery TeamContext 与 Step get DTO/CLI/Desktop 贯通。
- Schedule execution / management（definition polling、automation run dispatch、browser routes）：`internal/schedule` + `internal/store/schedule` + `internal/api/schedule`；稳定的 Trigger API facade 由 `internal/trigger` 转发到 canonical Schedule store，不读取旧 Trigger 表或解析旧 id
- AgentTeam MCP schedule facade tools：`desktop/internal/channel/agentteam/mcp_tools_schedule_compat.go`（保留旧 `trigger_*` tool wire names，内部只转发到 canonical Schedule API）
- Schedule Web management UI：`web/src/agentteam/workspace/triggers-route/TriggersRoute.tsx`（canonical route：`/schedules`；稳定 `/triggers` facade；共享 helpers：`web/src/agentteam/workspace/triggers-route/triggers.ts`、`web/src/agentteam/workspace/triggers-route/messageSourcePresentation.ts`）
- Service Actor substrate（App 统一安装态、能力、授权、target binding、webhook endpoint 基础表）：`service_definitions`、`service_installations`、`service_members`、`service_connections`、`service_grants`、`service_principal_authorizations`、`service_capabilities`、`service_target_bindings`、`service_webhook_endpoints`；当前 App API 仍是 facade，后续逐步切到这些 service_* 表。
- Service Actor store：`internal/store/serviceactor` 是 service_* 表的主数据访问层，并已挂到 `internal/store.Graph.ServiceActor`；它直接读写 service actor 表，不从 App 旧 store 反向取真值。
- App service-event subscription API：App page main path uses `/api/apps/.../service-event-subscriptions` and `internal/store/app` canonical `ServiceEventSubscription*ForUser` methods；stable `/provider-watches` routes and DTO aliases remain product facades over the same canonical service-event rows. Create/list/lifecycle and webhook dispatch read `service_event_*` rows directly with no Trigger-table replay, alias, or projection.
- Historical Service Actor backfill：`00166_service_actor_app_backfill.sql` remains migration history for onboarding existing App rows into `service_*` tables; current runtime does not invoke it, reverse-project, or depend on old App tables during rollout.
- Daemon WebSocket（`/api/v1/workspaces/:workspaceId/daemon/connect?key=...`，control plane）：`internal/api/daemon`（路由：`internal/api/daemon/routes.go`；Hub：`internal/daemonws/hub.go`）
- Core shared primitives：`agentteam-core/domain`（messaging / identity types + validation）、`internal/core/identifiers`（ID generation）、`internal/core/secrets`（token/hash helpers）、`agentteam-core/access`（workspace membership / channel / DM / reply-thread / visible-thread lookup、`workspace` / `channel` / `dm` / `thread` 等稳定 human authz 动作，以及 `AuthorizationRequest/Decision` + decision source / deny code contract）、`internal/store/message`（channel/DM/reply-thread access、channel/DM/member projection、DM create/reuse、agent target resolution、reply-thread open/list）、`agentteam-core/workspace`（workspace bootstrap rules，包括 slug 与 `#all` seed）、`internal/store/agent`（daemon control plane 的 machine/agent auth + start/disconnect primitives）
- 数据访问层（TiDB + 冷历史读取）：以 `internal/store` 为主（消息写入位于 message/store domain，冷历史对象与 driver 位于 `internal/archive`）；workspace bootstrap 与 daemon runtime auth/start/disconnect 已迁到 `internal/store/{workspace,agent}`，message-space access/target resolution 与 channel/DM projection/open-list 已迁到 `internal/store/message`，`internal/store` 保留兼容 adapter
- Task board 领域：`internal/store/taskboard/*`（list/get/create/update/set-claim/checkpoint、Task detail/review/activity），运行时使用 `coredb.DB` 与当前 Task schema。
- TiDB test helper + request-path regressions：`internal/coredb/testkit/tidb.go` / `internal/coredb/testkit/tidb_runtime.go`（优先复用 `AGENTTEAM_TIDB_TEST_DSN`；若裸跑单个 `go test` 包且未显式提供 DSN，则检查本机 `~/.tiup/components/{pd,tikv,tidb}/<version>` 二进制是否完整，必要时用 bounded `tiup install --force` 补齐，再自举共享 `TiUP playground` 的 `TiDB + TiKV + PD` runtime，并继续给每个测试创建隔离 TiDB database）、`internal/tidb/*_test.go`（admin/migration）、`internal/app/tidb_integration_test.go`（app startup）、`internal/store/taskboard/tidb_integration_test.go`（Task create/list/detail/update/claim/review/activity）、`internal/store/tidb_integration_test.go`（human send + list/sync + agent delivery/history）、`internal/api/org/*_integration_test.go`（org member/team/unit/grant/credential/Feishu sync 主链路）
- Org provider runtime：`agentteam-org/service.go`、`agentteam-org/verify.go`、`internal/app/app.go` 已把 org service construction / verify / startup wiring 收敛到 TiDB-only `coredb` seam；当前 main-path 与 enabled `agentteam-org` 已统一走 TiDB
- Agent local memory：runtime-local recovery memory 位于 agent workspace，布局为 `MEMORY.md` + `state/` + `kb/` + `archive/`；可迁移角色知识属于 Role package（`IDENTITY.md`、Skills、Knowledge），不与 local memory 混用。
- Core schema：`internal/tidb/bootstrap.sql` + `internal/tidb/migrations/*.sql`（fresh DB 先用 bootstrap snapshot，后续再应用 baseline 之后的 versioned migrations；`VerifySchema` 继续对 core + enabled provider 做完整性校验）
- Workspace placement cutover：`workspace_placements` 缺失时 data-plane admission fail-closed；历史 workspace 由 versioned migration 与 `agentteam-tidb-admin workspace-placement-backfill` 只补缺口，现有 placement 不被覆盖。production/staging activation 及 legacy `deploy-main` 均在 service restart/traffic 前执行 `agentteam-schema init → workspace-placement-backfill → workspace-placement-verify`，verify 失败即停止部署。
- 冷归档存储（filesystem/S3）：`internal/archive`（drivers：`internal/archive/filesystem.go`、`internal/archive/s3.go`）
- Web E2E 专用后端：`cmd/agentteam-e2e-server/main.go`（Playwright 通过 `make e2e-web` 启动；当前注入 Schedule execution service，并保留稳定 Trigger-shaped API facade 以覆盖浏览器回归）
- 仓库内置 desktop client 模块：`desktop/`（入口：`desktop/cmd/agentteam/main.go`；使用说明：`desktop/README.md`）
- 共享 Browser/Desktop 业务前端：根 `web/` 是唯一 React 业务应用；Browser/Desktop 通过薄 entry 注入 Runtime contract，共享页面统一调用 server API。Electron 将 Desktop renderer 打包进 `app.asar`，Go daemon 不嵌入 Web 静态资源。

补充说明：

- `desktop/` 是随仓库追踪的一方 desktop client 实现；用户默认接入命令是 `agentteam daemon`。
- Desktop 本地 HTTP 控制面只使用 `/api/internal/*`。active server 由 `GET/PUT/DELETE /api/internal/servers/active` 按认证后的窗口/UI session 隔离并携带递增 generation；它不是 daemon 进程全局状态。
- Desktop UI session 由 Go daemon 持有稳定 logical identity 和短期 bearer lease；Electron main 是唯一 lifecycle owner，统一处理 bootstrap、定时续租、息屏恢复、解锁、窗口聚焦及 renderer 上报的本地 `401`/`4401`。同一 daemon 下 bearer 更新只热替换 Web transport，不重建 Router、QueryClient 或业务 UI；旧 bearer 仅保留到原到期时间，logout/revoke 撤销同一 logical session 的全部 generations。daemon instance/base URL 变化才受控 reload，失败请求不自动重放 mutation。退出先关闭 reconcile admission、join 已开始的 daemon recovery，再 revoke。
- Electron 与 daemon 的本地控制协议为 Desktop API v2；新版只把已退役 v1 receipt 用于认证升级：无存活 Electron owner 时 adopt、shutdown 并替换 sidecar，owner 仍存活时要求用户完全退出旧版后重试，CLI/service owner 不由 Electron 替换。
- Desktop gateway 依次匹配构建 manifest 静态资源、`/api/internal/*`、HTML SPA navigation，最后把其余 HTTP/WS/SSE 按原 path/query 转发到当前登记 server。该 catch-all 覆盖 AgentTeam、MeshX、Git、文件等 server 路径，不引入 `/api/remote/*` 或 `/api/v1/*` 白名单。
- Browser 与 Desktop 的 Machine、Agent、消息、统计、activity、files 数据都使用同一 server API 语义。远程本机数据由 Server 通过 typed daemon transport 获取；客户端不直连远端 `/api/internal/*`。稳定边界见 `specs/daemon/desktop-architecture.md` 与 `specs/web/runtime-host-boundaries.md`。

运维健康检查端点（As-Is，**非**公开产品契约的一部分）：

- `GET /healthz`：进程存活检查，固定返回 `200 { "status": "ok" }`
- `GET /readyz`：就绪检查（2s 超时）；要求当前 core backend 可连通、没有 pending migration，且 schema 校验通过（`coredb.Admin.RequireSchemaCurrent`）；失败返回 `503 { "status": "degraded", "error": "..." }`，成功返回 `200 { "status": "ready" }`

核心原则：

- **消息的权威状态在服务端**，但不等于“所有历史永远放数据库里”
- **每个 thread 本质上就是 append-only log**：Channel 是一条日志，DM 也是一条日志；服务端协调成功的 rewind 会把目标热后缀写成 `rewound` lifecycle tombstone 并 scrub 派生投影，普通 edit/recall/rewind 都不依赖客户端本地删缓存
- **TiDB 只保留元数据 + 热消息正文**；冷历史是不可变归档对象，存到冷历史归档存储（生产部署建议 S3，本地开发/测试常用 filesystem）
- **不做多副本，不做高可用，不做分布式锁**；首版按单实例做扎实
- **不引入 Redis / SQS / Kafka / DynamoDB / OpenSearch**，避免把一个简单 IM 做复杂
- daemon 只负责本机 agent 生命周期与工作区；`deliver` 只做提示/唤醒，不承载可靠消息队列语义

这个模型特别适合本项目的原因：

- 最近消息几乎永远是热数据
- 消息默认 immutable：不支持普通原地 edit / delete；reaction 作为消息附属状态单独建模，不直接回写消息行；严格受限的 rewind 只通过 `rewound` lifecycle tombstone/scrub 让热后缀不可见
- channel access 仍刻意保持简单：`#all` 自动加入，普通 channel 以 membership 为唯一读写边界；`public` 只额外开放 discover/join metadata，不引入每消息 ACL
- 默认访问路径是“看最新一页、再向前翻”，而不是“从头扫完整个历史”
- 因此没有必要把数据库当作永久消息仓库

### 4.2 AWS 最小部署方案（面向 solo-maintainer）

首版推荐最小 AWS 组合：

- **EC2 单机**：跑 `agentteam-server`
- **TiDB（单集群）**：存元数据与热消息
- **S3**：存冷消息归档对象
- **SES**：注册 / 邀请 / 邮件验证

可选但非必须：

- **SSM Parameter Store**：放应用配置
- **CloudWatch Logs / Metrics**：集中日志与基础监控

首版明确不使用：

- ECS / Fargate
- ALB / API Gateway
- Cognito
- Redis / ElastiCache
- SQS / EventBridge
- DynamoDB
- OpenSearch
- Kafka

为什么这样更合适：

- 你已经明确接受**单实例、可中断升级**，那就没必要为了“未来也许会扩容”先上容器编排与负载均衡
- daemon 和 agent 本身就会反复重连 / pull，天然比传统 request/response Web 更能容忍短暂中断
- 把复杂度留在应用内可控边界，比把复杂度外包给一堆 AWS 服务更符合这个项目的规模

部署建议：

- EC2 上用 `systemd` 管理 `agentteam-server`
- TLS 直接在实例上的反向代理终止（例如 Caddy / Nginx）；这样不需要 ALB / ACM
- EC2、RDS、S3 放同一 Region，减少延迟与跨 Region 传输成本
- S3 bucket 使用通用 bucket 即可，不需要引入额外 S3 变种能力
- 冷消息归档对象默认放 `S3 Standard`，成本已经足够低，先不需要使用 S3 Lifecycle 降低复杂度

### 4.3 鉴权与身份（Human vs Machine vs Agent）

实现位置（As-Is）：

- Auth HTTP：`internal/appapi/handlers_auth.go`（路由：`internal/appapi/handlers.go`）
- Auth store：`internal/store/app_auth.go`（会话/refresh/token/用户变更）
- 密码哈希：`internal/passwords/argon2id.go`
- Token/secret：`internal/core/secrets/*`
- Mail driver：`internal/mailer/mailer.go`（driver 装配：`internal/app/app.go`；是否要求验证：`internal/config/config.go`）

#### 4.3.0 权限边界约束（Aligned）

按 `docs/architecture-spec.md` 与 `specs/organization/organization.md`，当前 core 的权限边界必须保持收敛：

- core 只负责：
  - human 认证与 session/token
  - workspace membership
  - channel / DM / thread 可见性
  - act-as 规则
  - agent / service capability issuance
- 当前权威事实仍然是：
  - `users.is_admin`
  - `members.workspace_role`
  - `scope_memberships`
  - DM scope 的 canonical `identity_key` 与双方 membership
  - `machines` / `agents`
- 新的 org ownership 目标要求 core schema 承载最小 organization ownership：
  - `workspaces.organization_id` 是 workspace owning organization 的唯一事实来源
  - `organization_settings.default_workspace_id` 是 organization 默认 workspace 指针
  - 完整 enterprise governance provider 仍然可选；core 不应引入通用 `roles` / `role_bindings` 或覆盖全系统的 `authz_resources`
- 当前 org 目标模型已明确为：最小 ownership 属于 core storage baseline；`organization -> member/team/unit -> workspace grant` 作为治理能力叠加到现有 workspace runtime primitive 之上。
- 当前代码落点已完成 ownership refactor 的 binding 清理阶段：TiDB schema / bootstrap 已引入 `workspaces.organization_id` 与 `organization_settings.default_workspace_id`，workspace 创建 helper、organization store 读写路径、auth 默认 workspace bootstrap、organization workspace 创建 API、`POST /api/v1/workspaces` 显式 `organizationId` 创建、Web 组织工作区创建 UI 已切到 owning organization；`organization_workspace_bindings` 仅作为历史迁移 `00108` 的输入存在，并由后续 cleanup migration 删除，不再是 runtime schema 或兼容写入表。
- 当前仓库里的 `agents` 表仍然是 workspace-scoped installation projection；组织成员授权不会隐式创建或安装 org 级 agent。
- workspace switch 继续以 `/api/v1/workspaces/:workspaceId/*` path identity 为准；org provider 只影响“哪些 workspace 对当前 human 可见”，不引入额外 org 级 request context。
- 当前 org 实现状态（As-Is）：
  - organization store 已落地 organization/member/team/unit/provider schema 与 workspace ownership / effective grant / revoke-reconcile / status-gate 逻辑；`workspaces.organization_id` 是业务 source of truth，workspace binding 表已从 runtime schema 清理
  - organization human membership 的事实源已收敛到 `organization_members`；workspace invite 会显式创建 owning org membership，但不会重新启用已停用的 org member；active workspace human member 必须拥有其 owning organization 下的 active organization member
  - authflow 已通过 organization store 执行默认 workspace bootstrap；identity store 不再直接创建 workspace，也不再持有 workspace 创建依赖
  - `agentteam-org/` 现已补齐 provider 侧 org governance backend 基线：`organization_invites` schema、invite create/list/revoke、token-based accept、org admin/member role update；加入 organization 仍不自动加入全部 workspace，workspace access 继续由 member/team/grant 编排
  - `internal/appapi` 已暴露 `/api/orgs/*` 管理已有 organization 的入口；web 端已具备 Organizations route、workspace Settings 中的 “Add from Organization”、workspace picker/sidebar org attribution、direct/team grant revoke、organization-owned workspace 创建；浏览器用户不再自助创建 organization，workspace bind/unbind UI/API 已从组织工作区主路径移除，workspace chooser 的创建入口也要求选择 organization
  - `/api/auth/invite-info` 与 `/api/auth/accept-invite` 现同时支持 workspace invite 与 organization invite token，保持统一 `/?invite=` 入口；但 org invite 的发送/管理 UI 与对应 Web E2E 仍属于 post-MVP 治理收尾项
  - Web 回归已覆盖 org 主旅程、回收旅程与状态门禁旅程；repo 级 `make e2e-web` 当前保持 green baseline
  - 当前 TiDB org portability 已完成：app startup、provider construction、schema verify、以及 `/api/orgs/*` 主链路都已经可以在 TiDB-backed deployment 上复用同一套 business path；本地 org-enabled smoke、rollback recipe、以及 performance/release-gate evidence 也已补齐。当前 canonical 口径需要写成：TiDB 已支持 `core main-path + enabled agentteam-org`；当前仓库还不能把 deployment 叫成 `operator-ready` 的原因仍是外部 operator gate：socket-capable org-enabled rollback live rerun transcript + target-deployment owner / observability / backup-restore sign-off
- JWT 只承载认证事实；跨服务信任的长期方向是短期 capability token，而不是把授权快照塞进 human token。
- `agentteam-core/access` 当前已固化 `AuthorizationRequest` / `AuthorizationDecision`：human 侧除了 workspace/channel/dm/thread 以外，也开始显式覆盖 `machine.manage`、`agent.operate`、`agent.read_workspace`；`users.is_admin` 也开始通过显式 `system_admin` break-glass request 固化 `workspace.access` contract，而不是默认混进普通 human membership 路径；runtime token 主语则通过 `agent_self` / `machine_self` decision source 固化 `agent.act_as_member` 与 `machine.connect` 语义；service capability 路径也已开始通过 `service_grant` decision source 固化 `service_installation.invoke` 的 installation / scope / act-as contract。store 适配层仍然向上翻译为兼容 API 的 `ErrForbidden/ErrNotFound`，但 core 内部决策已经带上 `decision_source`、`deny_code`、resolved principal/member，用于后续 act-as 与 provider/capability 收敛。
- `internal/store/access/scope_target_contract.go` 是当前消息域 target/access typed contract 基座：`ResolvedScopeTarget` 显式区分 requested target、storage/delivery/read/parent scope、authority subject 和 permission basis，供后续 attachment、agent send/history、service-event delivery、reaction/activity resolver 逐步替换旧的 generic `spaceID` / fallback 链。

#### 4.3.1 Human（人类用户）

人类用户鉴权完全走 **AgentTeam 自己的服务 + SES**，不依赖 Cognito。

首版建议直接采用最朴素的邮箱密码模型：

- 注册：`email + password + name`；系统初始化后默认由 `AGENTTEAM_AUTH_REGISTRATION_MODE=invite_only` 控制为邀请制，可用 `AGENTTEAM_AUTH_REGISTRATION_ALLOWLIST_EMAILS` 放行少量邮箱
- 密码存储：服务端只保存 `password_hash`，使用 `Argon2id`
- 邮箱验证：当配置要求验证时，服务端生成一次性 verification token，经 SES 发邮件
- 登录：校验邮箱密码；当配置不要求验证时，允许未验证用户在首次登录时自动补齐 `email_verified_at`
- PingCAP / AgentTeam User Center OIDC：userinfo 可通过 `email` 或已验证 `phone_number` 识别账号；当邮箱缺失但手机号已验证时，本地用户允许 `email=NULL`，并保存规范化后的 `users.mobile` 与 `mobile_verified_at`
- 会话：登录/注册成功后签发 `accessToken` + `refreshToken`
  - public API：`Authorization: Bearer <accessToken>`
  - 刷新：`POST /api/auth/refresh` 轮换 `refreshToken` 并签发新的 `accessToken` + `refreshToken`
  - 退出：`POST /api/auth/logout` 撤销当前 session（若提供 `refreshToken` 也会一并撤销）
  - 二维码登录的移动端 token 归属于 `user_session_families` 会话族；刷新继承原会话族，主动退出或账户端远程撤销会立即阻断该族的 access/refresh token
  - 账户安全入口通过 `GET /api/v1/me/mobile-sessions` 列出当前用户的有效移动端登录，通过 `DELETE /api/v1/me/mobile-sessions/:sessionFamilyId` 远程退出指定设备
- 密码找回：`POST /api/auth/forgot-password` 发起重置，`POST /api/auth/reset-password` 消费 token 重置密码（重置会撤销该用户的所有 session/refresh token）
- 登录/注册成功后会确保个人 workspace + `#all` + human member 存在（幂等）

public HTTP API（`/api/*`）约定（当前实现）：

- 鉴权：`Authorization: Bearer <accessToken>`
- workspace 选择：
  - `/api/v1/workspaces/:workspaceId/*`：workspaceId 在 path 中
  - `X-Workspace-Id` 只作为仍需要它的业务上下文 header；不再作为旧的无版本 workspace route fallback
  - 当前 workspace 选择与 workspace members/usage 等入口已统一走 `agentteam-core/access` 的 `workspace.access`，不再在入口层重复手写 human membership 查询
  - workspace member role update 也已收敛到 core 的 `workspace.manage_member_roles`，保持 owner-only 语义而不再依赖 store 层特判
- 错误体：`{ "error": "..." }`（少数场景额外带 `code`）
- public config：`GET /api/public/config`（无需鉴权）返回 `{ passwordAuthEnabled, pingcapAuthEnabled, registrationMode, messageHistoryDays }`
- `messageHistoryDays` 为当前 public config 的稳定 wire 字段名；其值对应内部的“hot message retention window”，仍由 `AGENTTEAM_MESSAGES_RETENTION_DAYS` 提供（见 4.4.4）
- 当前实现额外提供一个 product-specific thread inbox 入口：
  - `GET /api/threads`：返回当前 human member 已订阅的 reply thread scopes 及其父消息/父频道元信息
  - `POST /api/threads/:threadChannelId/subscribe`：为当前 human member 主动恢复 / 建立该 reply thread 的 inbox 订阅
  - `POST /api/threads/:threadChannelId/unsubscribe`：将当前 human member 从该 reply thread 的 inbox 中移除
  - 这组接口是 additive divergence，不改变已有 `/api/channels/:channelId/threads` 的 open/list 语义

为什么这里不用 magic link 作为主方案：

- 产品设计（`SPECS.md`）已明确首版登录方式是“邮箱 + 密码”
- magic link 当然也能做，但它会把登录、设备切换、邮件时效、重复点击等细节重新引入为主链路复杂度
- 对当前项目来说，自建邮箱密码 + SES 验证邮件是更直接、更稳定的最小方案

推荐的 auth 相关表（token 均以 `SHA-256` 存 `*_hash`）：

- `users`
  - `id`, `email`（PingCAP phone-only 用户可为 `NULL`）, `name`, `display_name`, `avatar_url`, `is_admin`
  - `password_hash`, `email_verified_at`, `mobile`, `mobile_verified_at`, `created_at`, `updated_at`
- `user_email_verification_tokens`
  - `user_id`, `token_hash`, `expires_at`, `consumed_at`, `created_at`
- `user_sessions`（access token）
  - `id`, `user_id`, `session_family_id`, `token_hash`, `expires_at`, `created_at`, `revoked_at`
- `user_refresh_tokens`（refresh token）
  - `id`, `user_id`, `session_family_id`, `token_hash`, `expires_at`, `created_at`, `revoked_at`
- `user_session_families`（二维码登录的移动端会话族）
  - `id`, `user_id`, `client_type`, `installation_id`, `device_name`, `device_platform`, `expires_at`, `created_at`, `revoked_at`
- `user_password_reset_tokens`
  - `user_id`, `token_hash`, `expires_at`, `consumed_at`, `created_at`

系统初始化（bootstrap admin）：

- DB 中是否存在 `users.is_admin=TRUE` 视为系统是否已初始化
- 若未初始化，`/api/auth/register` 会创建首个 admin（`is_admin=TRUE`，且直接视为已验证邮箱）
- 初始化完成后，后续注册均为普通用户（`is_admin=FALSE`）

本技术设计后续章节只依赖以下事实：

- 除 `/api/auth/*` 外，public API 的主要路由默认要求用户已完成邮箱验证（`email_verified_at != NULL`）
- 服务端可识别稳定的 `user_id`
- 一个 user 可以属于多个 workspace，并在每个 workspace 下拥有一个 human member

#### 4.3.2 Machine（daemon）

- daemon 通过 WS：`/api/v1/workspaces/:workspaceId/daemon/connect?key=<machineApiKey>`
- `machineApiKey` 是长随机串，只用于 machine 鉴权
- 当前实现将 key 明文存储在 DB（刻意简化），但 public API **不提供 reveal 旧 key**：完整 key 仅在创建/rotate 时返回（列表只返回 prefix）

因此第一版 `machines` 表对 key 的持久化直接使用单一字段：

- `api_key`

服务端行为：

- 创建 machine 时生成高熵随机 `machineApiKey`，原样写入 `machines.api_key`
- 校验 workspace-scoped daemon connect 时，直接按 `api_key` 精确查询 machine 并核对 path workspace
- 创建与 rotate 时返回完整 `machineApiKey`；平时列表/详情仅返回 `apiKeyPrefix`（避免把旧 key 反复暴露在 UI/API）
- daemon 收到 `401 {"error":"invalid machine api key"}` 时，将该 machine connection 视为本地 credential 已失效并停止该连接循环；
  用户需要通过 Web connect flow 或新的 `agentteam daemon --server-url ... --api-key ...` 重新写入有效 key。

为什么这版可以接受：

- 你已经明确要求第一版优先简化，不再引入 key 加密与 master key 管理
- `machineApiKey` 是服务端生成的高熵随机串，不是用户口令，不存在被穷举验证的产品路径
- 当前系统是单实例、低运维复杂度优先，直接明文存储能显著降低实现与运维心智负担

需要明确写下的代价：

- 数据库一旦泄露，所有 machine key 都会直接暴露
- 因此这只是**第一版的刻意简化**，不是长期理想状态
- 如果未来风险要求提高，优先升级为“可 reveal 的加密存储”；到那时再引入主密钥管理

安全约束：

- 生产环境必须强制 `wss`
- 访问日志必须脱敏 query string
- 同一个 `machineApiKey` 同时只允许一条活动 WS 连接
- owner 可以 rotate key；rotate 后旧 key 立即失效
  - 注意：仅更新 DB 不能让“已建立的 WS 会话”立即失效；rotate key 必须**主动断开**该 machine 的现有 WS 连接，迫使 daemon 用新 key 重连

#### 4.3.3 Agent / Instance（runtime data plane）

chat-bridge 会对 workspace-scoped Agent Runtime 请求带上：

- `Authorization: Bearer <token>`

当前实现里，各 runtime 的驱动实现和能力如下：

| 能力 | `claude` | `codex` | `opencode` | `kimi` |
|------|----------|---------|------------|--------|
| 进程模型 | stream-json CLI process | 长生命周期 app-server JSON-RPC 进程 | headless `opencode serve` process | 长生命周期 Wire JSON-RPC stdio process (`kimi --wire`) |
| 消息投递 | idle 时 stdin；active-turn 可走 ack-aware pending stdin | state-aware `turn/start` / `turn/steer` / single resume，以 `clientUserMessageId` 绑定 durable ownership，等 `userMessage.clientId` surfaced ack | 仅在 authoritative idle 后调用 headless `prompt_async`，以 fresh monotonic `messageID` 绑定 ownership，等 exact assistant `parentID` ack | idle/new-turn 使用 Wire `prompt`；`TurnBegin` 确认对应输入已进入 runtime |
| busy-turn 投递 | pending stdin + `--replay-user-messages` correlated ack 后完成 ownership | durable pending/uncertain steer；只在 correlated ack 或 authoritative absent reconciliation 后结束 ownership | 不直接写 OpenCode；保留在 daemon durable FIFO，待 idle 后作为新 turn 递交 | Wire `steer` 入队，后续 `SteerInput` applied event 才完成 correlated surfaced ack |
| rewind | capability-gated `--resume-session-at` | deprecated `thread/rollback` compatibility；迁移目标为 `thread/fork` + session-id rebind | capability-gated native revert | capability-gated completed-turn restore |
| rewind restore 方式 | 原生 `--resume-session-at`，before-first-input 用 fresh session boundary | 当前兼容路径使用 deprecated `thread/rollback`；不能把会生成新 id 的 `thread/fork` 原地替换，需先升级 restore/session persistence contract | 原生 `/session/:id/revert`；fork 不是 rewind strategy | detached-only、fail-closed 原子截断同一 session 的 `wire.jsonl` / `context.jsonl` |
| idle-compaction checkpoint | overflow 后 `/compact`；普通 idle no-op | 普通 idle 达共享 active-context threshold 后对同 thread 调用 `thread/compact/start`；overflow 也复用该 mutation 但保持 emergency purpose | 普通 idle 可按 active-context signal 调用 `/session/:id/summarize`；overflow 也复用 summarize | 仅 runtime-native compaction lifecycle |
| 流式输出 | stdout stream-json | 原生 notification（`item/agentMessage/delta` 等） | SSE translated events | Wire `event` notification translated events |

- proactive idle-compaction 的 Workflow boundary 由 `desktop/internal/agent` 的 `WorkflowCompactionGate` 随 exact Process generation 持有，并通过同一对象连接 model-visible runtime surface 与 Exec 的 Server-authoritative terminal result。Awaiting Step（包括穿插普通 turn）禁止 proactive check；只有 `workflow step complete` 成功或同结果 replay 允许随后 quiescent `TurnEnd` 检查一次。Fail、已确认 cancel 与权威 terminal error 只清 gate 并跳过对应 boundary；validation、not-ready、pause/park 和 ambiguous failure 继续保持 awaiting。该 gate 不属于 callback authorization、delivery gate 或 `isIdle`，不阻塞后续消息/Workflow Step，也不影响 overflow recovery。
- connector lifecycle：`serverconn.Manager` 在 terminal admission fence 下维护 per-server mutation generation 和 manager-owned lifetime context；Register/Enable/Reconcile 的旧 generation 不得发布或回滚覆盖新 generation，输掉 publication 的 runtime 必须 compensating stop。Stop 的短 deadline 只能让当前 waiter 返回，不能降级 immutable drain owner；同一 drain 会在后台继续并允许后续 strong waiter join。Wire 的每次 Session Link 也有唯一 run owner：所有 pump/handler/refresh/ticker/elastic-link/remote-assignment child 必须先注册再启动，断线或 shutdown 先关闭 admission 并 cancel，strong-join 后再 drain `sendCh`，旧 connection generation 不得向下一代发布 frame 或 catalog mutation
- profile persistence：每个 AgentTeam server snapshot 持有单调 revision；update/delete/补偿恢复均使用 exact-revision CAS，避免 A→B→C→B 的值相同 ABA 让陈旧 rollback 覆盖新配置。Dashboard 的 runtime/config transaction 与 Wire 的 workspace/status field-local persistence 都在 revision conflict 后重读 durable file、只重建目标字段并 bounded retry；内存状态只在 durable commit 后更新。Delete 先 suspension exact runtime generation，再按 revision 删除；配置失败时也只能恢复同一 generation，不能复活已被并发操作替换的 runtime

- `codex`
  - daemon 持有的长生命周期 Codex host 进程，通过 stdio transport 与 Codex app-server 建立 JSON-RPC 连接；app-server 是内部实现机制，公开 runtime ID 只有 `codex`
  - 握手协议：`initialize` request + `initialized` notification（两步必须完成）
  - 线程管理：通过 `thread/start` 创建新线程或 `thread/resume` 恢复已有线程；维护 `threadID`、`threadStatus`（含 `activeFlags`）、`activeTurnID`、completed-turn/non-steerable/compaction fence 等协议派生状态；Codex 声明 explicit turn lifecycle，只有 authoritative `turn/started` / terminal boundary 改变 root busy/idle
  - prompt surface：fresh thread 把平台级核心规则与 agent-specific 指令分别放到 `baseInstructions` / `developerInstructions`，只把可被 compact 的 skill reference 和动态消息内容放进首个 turn input；真正 resume 时重放同一 launch-generation policy snapshot，空 optional override 不发送。当前 instructions 发生变化时由 fingerprint 触发 fresh thread，不能把 resume 当成 thread 内热更新
  - prompt compiler：`desktop/internal/promptcompiler/*` 现在是 prompt canonical boundary；它负责把 thread contract / turn context / live delivery 统一编译成 `RuntimeInputEnvelope`、`PromptBundle`、`DeliveryEnvelope`，并由 `desktop/internal/dispatcher/prompt_compiler.go` 收集输入后调用 compile/render
  - Token 诊断：`desktop/internal/tokenanalysis` 是 daemon-owned report/compare owner，`desktop/internal/api/tokenanalysis` 只提供 Human owner 与 Agent self-only adapter，`desktop/internal/cmd/token.go` 提供唯一的 `agentteam token analyze|compare|ui` 入口。详细高基数采集默认关闭、仅属于 daemon lifetime；Human analyze 自动开启，UI 使用同一开关，普通 per-Agent usage 不受影响。存活 Process 只保留无正文 launch attribution 以便开启时补记；delivery attribution 只在 surfaced 或可能已入模时提交。Runtime usage 复用 driver 统一口径：input 是包含 cache read/create/write 的 inclusive total，cached 只是 cache-read 子集，total 不重复加 cached；OpenCode 用 `step-finish` part ID、Kimi 在存在时用 root `StatusUpdate.message_id` 作为 invocation identity，并把 Process active semantic blocks 记录为 invocation exposure；OpenCode 成功 compaction 会先淘汰 `compactable` block，Kimi subagent 不套用 root Context。Codex turn 与 Claude result 不冒充 provider call identity，因此其调用次数和 exposure 仍为 `unsupported`。当前 Run 是 launch-level 聚合，task outcome 与 parent/child edge 仍不支持
  - prompt packs：compiler-owned repo prompt sources 已落到 `desktop/prompts/{platform,agent,events,notices,channel}/*.md`，由 `desktop/prompts/prompts.go` embed + render；channel skill docs 与 identity notice wording 也已迁到这些 prompt packs，trace/source-map 现在可直接回指对应 markdown template 路径，而不是继续把 `text.go`、`skilldoc.go` 或独立 formatter 当 source of truth
  - session migration：daemon 持久化 `baseInstructions + developerInstructions` 的 fingerprint；legacy 无 fingerprint thread 或 fingerprint 已变化的 thread 会直接 cut over 到 fresh `thread/start`，避免已有 thread 继续执行 stale pre-split instructions
  - 消息投递：router 从 app-server state 选择动作：regular active + turn id 走 `turn/steer`，idle/systemError 走 `turn/start`，notLoaded 最多 resume 一次，unknown/active-no-turn 最多做一次 `thread/read(includeTurns=true)`；read 后仍 active-no-turn 保持 ordered boundary，绝不把 `turn/start` 当 active probe。non-steerable/compaction fence 不被 refresh 绕过。所有 routed start/steer（包括 interrupt-based transient recovery）都把 daemon delivery id 写入 `clientUserMessageId`。单个 concrete input 最多一次 refresh、两次 mutation；ambiguous timeout/EOF/decode/transport failure 第一次 mutation 后立即停止，interrupt expected/found mismatch 最多用 actual id 重试一次。
  - delivery ownership：daemon 使用 `queued`、`inflight(dispatching|outcome_unknown)`、`terminal(surfaced|failed)` 三态 journal。同步刷盘只守住三条不可逆边界：daemon ACK 前 durable queued、runtime mutation 前 durable inflight、释放 owner 前 durable terminal；retry/backoff/wake UI 等过程状态不要求磁盘快照持续最新。凡带 concrete `MessageID` 的 default dispatch、`agent:start` wake 和 hidden workflow 都使用这条边界。当前 routed MessageID 是 foreground admission lane：历史 queued/inflight restore 与 reconcile 不能把当前 wake 替换成空 launch或拒绝 durable admission；workflow callback、wake lifecycle 与 transient recovery 仍是 runtime mutation 前的安全 fence，受阻的新 owner 留在 durable queue。Journal 容量为 512 live/8 MiB、512 terminal/2 MiB、1024 total/10 MiB、单条 live 512 KiB；新 MessageID 的首次 live admission（queued 或 direct-mutation 前的 inflight）与 oldest-live→terminal failed reclaim 在同一 state transaction/fsync 中完成，已有 owner 状态转换不选择 reclaim victim，failure code 为 `ownership_backlog_reclaimed`，随后 Process 投影删除 queued owner、保留 inflight 晚到 correlated ack 并发 wake/runtime diagnostic。terminal tombstone 去掉 replay body，failed 可被迟到 authoritative surfaced 升级但不可恢复 live。旧 unversioned ownership schema 在加载时迁移，未知未来版本不做语义猜测。
  - 可见回复边界：`turn_direct` 只描述消息进入 runtime 的投递方式，不表示 runtime 纯文本会自动成为 AgentTeam 消息；正常可见回复仍以 `send_message` 成功写入为准。daemon 会记录 `reply_sent` / `completed_without_visible_reply` 证据，不会把 stdout、final text 或本地 transcript 自动 relay 成普通消息。可见回复契约（VisibleReplyContract）始终启用（always-on），prompt compiler 会在后续 wake / resume / delivery 的动态输入中增加可见回复契约指引；该投影不是 runtime policy / thread contract identity，不触发 runtime restart 或 fresh_start。历史 per-account / per-agent 关闭值不再作为有效配置输入，服务端不会基于其下发或响应 false；对仍声明 `agent:visible-reply-contract-v1` capability 的旧 desktop 固定投影 `enabled=true`，历史 DB 中残留的关闭值不再产生有效关闭效果。Daemon 为每条 concrete delivery 保留 `MessageID`、canonical `TargetKey`、target-local `TargetSeq`、delivery class 与终态，但普通 visible-reply obligation 按 target 聚合，不使用逐消息本地凭据结算。只有服务端成功响应 `sent` / `already_sent` 携带 `TargetKey` + `SeenUpToSeq` 时，才批量结算该 target 下已 surfaced 且不超过 frontier 的 obligations；held、失败、本地 AcceptedSeq、旧 frontier 与其他 target 都不结算。更高 seq 在后续边界重新触发 fresh final check；signal-only delivery 不触发普通 final check。若最终仍未 send，相关消息一次性 terminalize 为 `completed_without_visible_reply`（或 `failed`）并释放状态。Context recovery 的 target 规则见 [`specs/daemon/wake-visible-reply-contract.md`](../specs/daemon/wake-visible-reply-contract.md)。
  - freshness / reconcile surface：start/steer transport accept 不包含“已入模”语义、不推进 observed；只有 correlated `userMessage.clientId` surfaced ack、daemon 自动递交 fallback target messages、或显式 `read_history` 返回 target coverage 才推进 observed。Ambiguous/restart reconciliation 每次只读一次 old thread 的 full turns/items：matching id=surfaced，active missing id=pending，incomplete history=unknown/pending，只有 complete inactive history missing id=absent；reconciler 不 resume old thread、不内部 poll，且只调度 oldest live head。每个 attempt 捕获 record generation/store attachment epoch；late surfaced 或 phase transfer 递增 generation，陈旧 read 结果不得覆盖 live mutation。unknown/error 有六次短预算，authoritative pending 使用 15 分钟 safety ceiling而不是短 ACK timeout；耗尽只 durable fail，不 blind replay。同 target pending refresh 的 held-like result 仍按 local coverage 提示等待 input boundary 或 exact-target `read_history`；server 不提供 agent `/receive`，daemon journal/inbox 不暴露 agent pull tool
  - MCP freshness final-hop transaction：本次 freshness page 不再提前写 surfaced。它在同一锁/持久化临界区把 FIFO head 从 queued 变为带 128-bit opaque receipt 的 uncertain，保留原 AgentTeam MessageID/正文作为 canonical owner，并在移出 inbox 前安装 bounded final-hop lease；熵失败或 receipt collision 保留 queued，live receipt 又会阻止第二页 q2 越序。本次 claims 与历史 message-id dedup 分流，receipt marker 前置到 tool text，validated receipts 放入 MCP `_meta` 且穿过 result budget/held-send 外层包装。普通 MCP 只有完整 HTTP response write 才 commit surfaced；Codex HTTP 只返回 Deferred，必须等同 generation 的 app-server JSON-RPC child write 完整成功后才 emit surfaced。short/partial write、stale generation、callback/transport failure转 uncertain；claim-time lease覆盖“claim 返回到 HTTP callback”窗口，Deferred只续租，丢失 callback 在六分钟（长于 Codex 五分钟 request timeout）后进入 exact-thread marker reconciliation。`thread/read(includeTurns=true)` 中 exact marker=surfaced，active missing=pending，incomplete=unknown，只有 complete inactive missing=absent 后才 exact replay
  - 显式打断：用户希望 agent 停止当前工作时必须通过 UI / API 显式 interrupt 入口，不从消息正文推断；规范见 [`specs/message/agent-interrupt-control.md`](../specs/message/agent-interrupt-control.md)
  - lifecycle fence：root delta/item/error、async MCP/dynamic callback 与 terminal notification 共享 turn-event serialization；只有 current primary、current turn 且未 completed 的 activity 可发布。`error(willRetry=false)` 保留错误证据并合成一次 TurnEnd，后到 completed 被 consume；late `userMessage.clientId`、usage/settings、subagent control-plane metadata 仍可完成 ownership/观测且不重开 root turn
  - protocol 状态刷新：daemon 维护 `threadID`、`threadStatus`、`activeTurnID` 等协议派生状态，用于 state-aware delivery、recovery、legacy rollback 和 compaction；本地状态不确定时通过 bounded `thread/read(includeTurns=true)` 刷新。新 `turn/started` 可替换 stale active cache，但 completed same-id started 与 late completed-turn activity 必须被 fence
  - rewind：额外维护稳定的 app-server `threadId` 与 checkpoint。当前 live/detached restore 仍是 deprecated `thread/rollback` compatibility path；OpenAI Codex 推荐 `thread/fork(lastTurnId|beforeTurnId)`，但 fork 产生新 id，因此完整迁移必须原子 rebind Process/dispatcher/session projection/ownership 后才能删除 rollback
  - recovery / restart safety：在 `NewProcess`/driver `Spawn`/任何首个 turn mutation 之前先持久化 unsafe baseline；live ownership upsert 同事务置 unsafe，safe promotion 事务再次拒绝任何 live journal owner。Live recovery 优先用 `thread/read` 分层判断状态，必要时 `turn/interrupt` 中断 active turn后调用兼容 rollback；detached recovery 启动临时 app-server 进程后固定走 `thread/resume(excludeTurns=true)` → authoritative `thread/read(includeTurns=true)` → 必要时 interrupt +再次 read →兼容 rollback。`thread/read(includeTurns=true)` 虽可读取 persisted/notLoaded 历史，却不会装载 live ThreadManager；fresh/detached rollback 因此必须在兼容 mutation 前 resume，而 exclude-turns 的 resume response又不能替代 detailed read。Restore 与 delivery 共用 `deliveryGate`，rollback 前同时证明本地容器和 durable journal 无 owner；cleanup 只事务清 resume/recovery 字段并递增 epoch，保留 ownership、deferred、task 与 runtime policy。SessionInit/TurnStarted/handoff/compaction 标 resume-unsafe，只有 no inbox/pending/reserved handoff 的 quiescent authoritative terminal boundary才能标 safe；surfaced ACK清掉最后 pending 后再次尝试 safe promotion。callback 用 versioned snapshot replay防注册竞态。Daemon restart遇到同一 saved Codex thread仍 unsafe时 fail closed创建 fresh thread，但 ownership reconciler可用 record中的 old thread id做read-only查询
  - compaction：Codex runtime 仍可根据 active history/model limit 自行 auto-compact；AgentTeam 同时在 quiescent ordinary `TurnEnd` 使用 primary thread 最新 `thread/tokenUsage/updated.last.totalTokens`、共享绝对阈值与 cooldown 做 proactive due check。Driver 用当前 launch、root `threadId + turnId + contextCompaction item.id` 的 exact started/completed pair 观测 boundary：匹配普通 root turn 为 `runtime_native`，匹配 AgentTeam proactive/manual turn 为 `agentteam_proactive`，匹配 terminal-overflow queue 为 `agentteam_emergency`，缺失/冲突为 `unknown`。Started 保存最近 root usage，active item 保存最后 exact-turn usage；completed 后只有第一条 root usage 获得 `ContextCompacted`，foreign/stale usage 不能消费 latch。`contextWindowExceeded && willRetry=false` 只 queue emergency purpose，并把 overflow cause turn 贯穿 queue/start/result/item diagnostics；proactive purpose 不产生 continuation，matching terminal failure 也不重新 queue overflow。App-server 可先发 `thread/status/changed idle`、后发 matching `turn/completed`；driver 只在当前 generation/compaction turn 上 latch 前者，并由精确 matching completion 消费后依次 emit `RuntimeCompacted` 与 `RuntimeIdle`，Activity 明确记录 compact 成功后再进入 idle；exact before/after usage 可用时 completion detail 同时带 active-context token count。Wrong/duplicate/old completion 与 ordinary turn 不消费。Compact mutation 一旦可能写出，timeout/EOF/decode 等 ambiguous 结果保持 fence/non-idle/resume-unsafe，直到 authoritative idle；不能回滚状态后重发第二次 compact
  - 流式输出：通过原生 JSON-RPC notification 驱动，包括 `item/agentMessage/delta`（文本流）、`item/reasoning/summaryTextDelta`（可见 thinking summary 流）、`PlanDeltaNotification`（plan 增量）、`item/commandExecution/outputDelta`（shell 输出）、`item/fileChange/outputDelta`（文件变更）等；输入 reader 用 `ReadSlice` 累积完整行并执行含换行的硬 `32 MiB` 上限。Translated event 先按最终 JSON wire bytes 计量，再进入 generation-scoped lossless bounded queue；饱和时 backpressure，replacement/cleanup 先 abort 旧 queue，source EOF/malformed 则 seal 并 drain 已接纳输出，Process exit 要等 reader、flusher 与 final-EOF consumption fence，oversize/encode/short-write failure 不能静默 drop。AgentTeam 启动 Codex turn 时按 `AGENTTEAM_CODEX_REASONING_SUMMARY`、用户 Codex `model_reasoning_summary`、`auto` fallback 的顺序解析 summary mode，避免覆盖显式用户配置但仍保持默认 summary 可见。`item/reasoning/textDelta` 是 raw reasoning 流，默认不进入 AgentTeam activity，只在显式开启 raw reasoning 调试时展示
  - 原生 thread/turn 协议接入设计见 [`docs/codex-app-server-native-protocol.md`](./codex-app-server-native-protocol.md)
- `opencode`
  - Desktop release artifact 内包含平台对应的 AgentTeam-managed OpenCode 可执行文件；daemon 解析 `builtin` runtime 时使用该 managed binary。缺失、不可执行或 build-time expected version 不匹配时，错误语义是 AgentTeam Desktop 安装不完整，而不是提示用户安装 npm/opencode。`opencode` runtime 表示外部 OpenCode binary，通过 PATH 或手动 binary path 配置使用。
  - `builtin` runtime 的 XDG data/state/cache 持久化在 `~/.agentteam/agents/<agent-id>/.runtime/opencode/`，每个 agent 独立；dispatcher 通过独立于 launch workdir 的 `RuntimeStateDir` 显式传入该路径，因此 session、ephemeral 与 detached recovery workdir 不会改变或回收 SQLite。启动前按环境变量名大小写不敏感地移除 `OPENCODE_DB`、`OPENCODE_TEST_HOME` 及全部 managed key 别名，再以 canonical key 写回强制配置；其 SQLite 位于对应 `data/opencode/` 下，不与用户安装的 OpenCode 数据库或其他 agent 共用。AgentTeam-managed config 继续使用 agent workdir 内的专属目录。外部 `opencode` 保留用户原生存储与 `OPENCODE_DB` 语义。
  - 当前 driver 运行 `opencode serve` headless server，经 HTTP `prompt_async` 投递 prompt，并通过 SSE 翻译 text/tool/status/usage 事件
  - headless permission 由共享的不可变 launch policy 在启动时直接配置：full-access（含归一化为 full-access 的旧别名）写入 wildcard `allow`；内部 `DisableNativeTools` restricted launch 写入 wildcard `deny`、只允许 Knowledge namespace。已退役的 `knowledge_qa_scratch` permission mode 不是兼容别名，driver 必须拒绝；server-hosted Knowledge Q&A 不启动 Desktop Agent runtime。正常路径不产生 ask。串行 SSE `permission.asked` handler 仅作为上游变化或异常配置的协议兜底，按同一 policy 单次批准或拒绝；任何非成功 reply 都发出 bounded runtime error 并停止 OpenCode process，由既有 process lifecycle 负责失败、turn end、重启与 queued input 恢复。runtime diagnostics 只记录 request/session/tool correlation、permission 与失败结果，不记录 permission metadata、raw pattern 或 tool input
  - `prompt_async` 仅在 driver 已观察到 authoritative idle、没有 unresolved correlated input 且没有未确认的 Runner-admission reservation 时提交新正文；active turn 期间消息继续由 daemon durable FIFO 持有。原因来自 OpenCode v1.17.11 源码：`createUserMessage` 的 row/parts 是分开写入，而 `Runner.ensureRunning` 在已有 runner 时等待旧 run 后会丢弃第二个 caller 传入的 work，因此 active `prompt_async` 的 204 不能充当可靠 enqueue。idle 提交时 driver 在 HTTP mutation 前安装通用 Runner-admission reservation，并给正文分配 OpenCode-compatible、不会随本地时钟回拨的 monotonic `messageID`；只有后续 `busy` / `retry` 或 exact assistant `parentID == messageID` 才证明 fork 已进入 Runner。旧 turn 的 prune/tool/part/delta/assistant 更新不能清掉新 reservation。Runner admission 后也不能只凭同 session 或 logical busy 接受 activity。driver 为当前 Runner 维护 user/assistant/compaction/summary message-id 集，assistant 必须 parent 到当前 prompt、当前 compaction 或 source-bound synthetic/replay continuation，tool/step/text part 则必须引用已接纳的 assistant id；不能用 message-id 数值顺序替代这些绑定，因为 OpenCode 的 ascending ID generator 在 wall-clock 回拨时也会回拨。这样上游在 `Runner.finishRun` 后异步发布的旧 `compaction.prune` part，即使晚于下一 turn 的 fresh busy，也不会重新把 driver 置 busy、泄漏旧 tool output 或清掉 late-error fence。204、matching user `message.updated`、`session.error`、`/abort` 200 或 admission 前的 `idle` 都不能清掉 reservation、合成 turn end 或开放第二次 prompt。普通输入只由 assistant 的 exact parent 结算 surfaced ownership；但上游也可能在新 root user 进入 Runner 后先根据上一 finished assistant 触发 automatic compaction，因而永远不创建 parent=root 的 assistant。此时 current-Runner `auto=true` compaction part 是 source proof，driver 原子地把 pending correlation 从 root user 迁移到 compaction user，仍由后续 summary assistant 的 exact parent ACK 结算；即使插件关闭 autocontinue，也不会留下永久 owner。`session.error` 会先作为 nonterminal diagnostic 发出，因为 OpenCode 会在 file/directory 读取失败后把错误转换为 synthetic text 并继续 runAgentTeam；后续 correlated positive activity 会清掉 terminal candidate。若 generic fatal error 保留到 authoritative Runner idle，或仍有 exact-parent correlated owner，driver 会先 invalidate 并停止整个 server generation，再发 terminal error，不把它当成可复用 idle：Runner 可能在创建 assistant parent 之前失败，而 `Runner.finishRun` 还会先发 idle、随后 `prompt_async` 外层 `catchCause` 才发 wrapping error；继续复用会分别造成 correlated owner 永久悬挂或 post-boundary error 把 Process 永久改回 busy。context overflow 是 source-derived 双 idle 状态机：`compaction.auto=false` 时 processor 先发 idle，随后持久化 matching completed/error assistant cleanup，最后 Runner 再发 idle；driver 在第一 idle 保持 logical busy 并 suppress cleanup，只有第二 idle 才发 terminal Error+TurnEnd并允许 native summarize。OpenCode 自动 compaction 成功则以 `session.compacted` 继续原 Runner，不触发 AgentTeam summarize；所有 `summary:true` current-Runner assistant 的 text delta 只用于 OpenCode 内部上下文、不会作为 AgentTeam 回复输出，而后续非 summary continuation 仍正常可见；matching summary assistant 失败会带 `context_compaction_failed` retire/fallback。若 accepted prompt 在 30 秒内始终没有 correlated Runner 证据（包括只有 `session.error` 且没有真实 Runner idle 的 `catchCause` 路径），watchdog 同样停止整个 generation，绝不开放第二次 mutation。native interrupt 在 Runner admission 或 exact-parent ack 未决时 fail closed；其余 interrupt 吞掉上游 `MessageAbortedError → processor idle → cleanup → Runner idle` 整段，cleanup drain 穿过 live revert，直到新 prompt 的 fresh busy/retry 才开放新事件 generation。response-less mutation 保持 `outcome_unknown`；当前 generation 没有 authoritative read reconciler 时等待迟到 exact parent，Process 替换后的 restored owner则 fail closed，任何路径都不 blind replay。
  - OpenCode SSE 不提供 replay cursor；driver 从 Spawn 到首个 data frame都关闭新 prompt mutation，并允许在尚未 ready 时重试连接。一旦 ready 后的 stream 丢失，driver 立即停止整个 server generation，而不是连回一个无法补帧的新 stream；若已 dispatch prompt 的 parent/status frame 在断线窗口丢失，其 reservation/owner 保持 fail closed，不能推断 absent 或重发正文，由 Process generation replacement 收敛。所有 retirement 都在同一锁内推进 session/summarize generation、关闭 in-flight summarize retired fence，并在任何可能阻塞的 translated event 投递前执行 process cancel，旧 HTTP/SSE callback 因此不能为 replacement generation 伪造 idle/error/turn end。
  - ordinary `TurnEnd` 会在 visible-reply final-check、delivery terminalization、context-overflow recovery 和 inbox drain 之后进入一次 proactive idle-compaction checkpoint。Process 持有与 delivery 相同的 admission gate，并要求无 inbox、pending/uncertain runtime input 或 handoff owner；因此 due check 与 compaction mutation 之间到达的消息只能排在 compaction 后，不能并发写 runtime。OpenCode 与 Codex 都实现同一 capability interface，并使用共享的 active-context 绝对阈值（默认 150,000，`AGENTTEAM_PROACTIVE_COMPACTION_TOKENS=0` 可禁用）和两分钟 cooldown；公共 Process 不按 runtime name 分支。OpenCode 从最新 `message.updated` 读取 input context 并执行 same-session summarize；Codex 从 primary thread 最新 `thread/tokenUsage/updated.last.totalTokens` 读取 active context，在权威 idle 时对同一 thread 调用 `thread/compact/start`，且 runtime-native compaction completion 进入同一 cooldown，避免立即重复 manual compact。Codex overflow queue 始终优先并保留独立 emergency purpose/continuation；matching proactive terminal failure 只作为 maintenance failure 回到同 thread idle，不 queue overflow recovery。成功 compaction 先发 `RuntimeCompacted` Activity/trajectory 记录，再由 `RuntimeIdle` 恢复 idle 并优先投递排队 input；两者都不注入 continuation，也不再次触发 checkpoint。OpenCode proactive failure 在 retire generation 后以 maintenance-tagged、bounded `context_compaction_failed` Error+TurnEnd 收敛；maintenance attribution 要等本次 summarize 的 fresh busy/retry，之前到达的 delayed `prompt_async` wrapper 仍归属上一 turn。Process 不把真正的 maintenance failure 解释成 overflow recovery，不 rotation、不重试已经成功的普通工作。Claude 当前缺少可信 active-context signal，Kimi 缺少 AgentTeam-triggered same-session compact API，因此保持 no-op/native lifecycle。
  - OpenCode native summarize 的 HTTP response 与 SSE 是独立证明：driver 从启动到结束持续持有 `summarizeInProgress`，只有 HTTP 200、本 generation 的 `auto=false` compaction part + matching summary assistant、fresh `busy → Runner idle`、以及成功专属的 `session.compacted` 全部出现后才发 `runtime_idle` 并重新开放 prompt。要求本次 summarize 自己的 busy/compaction identity 是因为 Process 消费上一 turn_end 后，SSE transport 里仍可能有重复的旧 idle/compacted frame；旧帧不能提前满足新 fence。HTTP/transport failure、缺任一 SSE success proof、summary/session error、超时或 stream loss一律先 retire generation，再发 `context_compaction_failed` Error+TurnEnd，不把 HTTP 200 或 processor idle当成 compaction success。未能由 Process 启动的 stale `compactQueued`（例如 recovery disabled 或 preflight failure）不会永久挡住后续 prompt；一个成功的新 prompt reservation会消费该废弃请求，之后若再次 overflow 可重新排队。serve 退出前所有 SSE/summarize producer 先完成，translated output 再 lossless drain，并等 `parseStdout` 处理最后一行后请求 EOF，Wait 才向 Process 暴露退出；显式 Stop 可 abort drain，超出 process line limit 的事件则 fail closed retire，避免 terminal boundary 丢失或 writer deadlock。
  - resumed session 在任何新 message id capture/initial prompt 之前分页扫描完整 raw message history并提升本地 id high-water；这一步不能用 `limit=1` 代替，因为 OpenCode page 按 `time_created` 排序、`MessageV2.latest()` 却按 id 最大值选择。raw high-water 覆盖所有行；resume candidate 则先按上游 `MessageV2.filterCompacted` 的 newest-first membership 去掉 completed compaction boundary 之外的旧历史，再只在 effective membership 内选 lexicographic max id，避免旧 compacted row 因时钟回拨反而被选中。scan 对非标准 ascending message id 或 effective max assistant/max user 失配 fail closed，不能在无法证明新 id 排序时继续 mutation。无新 turn input 的真实 resume 中，unanswered max-id user 使用自身 `messageID`，unfinished assistant/tool 使用其 parent user `messageID` 单次 empty-parts bootstrap，terminal assistant 不 kick；若 max user 是未完成的 compaction task，scan 同时保留其完整 part 与 `auto` mode，并在 bootstrap HTTP mutation 前预先绑定 current-Runner compaction root。新 server 的 runAgentTeam 会从 DB 继续该 task、却不会重新发布旧 compaction part；预绑定因此是 summary suppression、auto continuation、失败归类和最终 idle 收敛所必需。缺失 `auto`、summary 无 compaction parent、bootstrap 或 high-water scan 失败都直接使 Spawn 失败。
  - rewind strategy 为 `opencode_session_revert`：使用 OpenCode 原生 `/session/:id/revert`，recovery ref 绑定实际 `prompt_async.messageID`；AgentTeam-managed OpenCode 强制 context-only 配置，发布前通过源码检查和 LAN E2E 固化该语义
- `claude`
  - 当前保留正常消息 / tool use 行为
  - 支持 stdin idle delivery：daemon 在进程存活且处于 idle 时，可直接通过 stdin 投递下一条消息
  - active-turn delivery 使用 commit-aware pending stdin：daemon 以消息 ID 作为 `uuid` 写入 Claude stream-json stdin，并使用 `priority: "next"`；Claude 以 `--replay-user-messages` 在真正把该 user message/queued command 纳入 query boundary 时回放 `type=user,isReplay=true,uuid=...`。AgentTeam 在 transport 写入成功后立即按正常 `prompt_input` activity 展示消息正文（不暴露 pending/queue/boundary 文案，且作为 background notice 不替换现有 command activity）；它不推进 observed、不标记 surfaced。surfaced ack 到达后追加轻量 agent-ack activity、记录 recovery surface、标记 surfaced message，并推进该 target 的 observed freshness frontier。
  - active-turn surfaced ack 消费前会重新检查 freshness 与 session identity：若同 target seq 已经通过自动 fallback delivery 或 `read_history` 观察，daemon 只清理 pending 状态；若 ack 来自不同 session，daemon 不把该 ack 当成 surface，并把原消息放回 local continuation inbox 走自动 fallback delivery。两种情况都不会重复展示具体 `prompt_input`、不会重置 wake lifecycle、也不会推进 freshness。
  - ack-aware active-turn contract 当前适用于 Claude (`--replay-user-messages`/`uuid`)、Codex (`turn/steer`/`clientUserMessageId`/`userMessage.clientId`) 与 Kimi Wire (`steer`/`SteerInput`)；OpenCode 明确不声明 active-turn capability，而是在 idle/new-turn 上使用 `messageID`/assistant `parentID` correlated ownership。若对应 runtime 的 acked delivery 不可用、写入失败或 session mismatch，daemon 保留 durable continuation 并在安全 input boundary / idle 自动重试；transport accepted 的 pending input 不使用固定 ack timeout
  - `claude_session_resume_at` 是 Claude Code 的 rewind strategy：使用原生 `--resume-session-at`，before-first-input 用 fresh session boundary；发布前通过 driver restore/verify 与 LAN E2E 验收
- `kimi`
  - daemon 启动 `kimi --wire --session <id> --work-dir <dir>`，使用同一 JSON-RPC transport 完成 initialize、prompt、steer、cancel、resume 与 runtime event 翻译；AgentTeam 预生成 session id，并在显式 controlled resume 时要求恢复同一 session。
  - idle delivery 的 `prompt` response 和 busy delivery 的 `steer` response都只表示 transport accepted；`TurnBegin` / `SteerInput` event 才按 daemon delivery id 完成 surfaced。crash reconciliation 只读该 session 的 `wire.jsonl`，完整 inactive history 缺少 correlation 才可判 absent，不确定或 active history保持 pending/unknown。
  - `StatusUpdate.token_usage`、content、thinking、plan、tool、approval、`TurnBegin` / `TurnEnd` 映射到公共 runtime event；approval 与未知 server request fail closed。显式 interrupt 使用 Wire `cancel` request，不能用 cancel + prompt 冒充 steer。
  - rewind strategy 为 `kimi_session_turns`。recovery point 记录输入入模前的 completed-turn count 与真实 user-input count；detached restore 在路径校验后通过可恢复 journal 原子截断 `wire.jsonl` 和 `context.jsonl`，context 统计排除 synthetic checkpoint，并保留同一 turn 内的 steer input。Kimi 不声明 live restore。
  - Kimi tool-restricted launch 为每个 session generation 使用稳定的私有 `KIMI_SHARE_DIR`：每次 launch 只临时复制允许的 runtime config/OAuth credentials，不复制 plugins，退出时删除 config、credentials 和 launch files，但保留 provider session transcript 供普通 controlled recovery；launch-scoped `--agent-file` 按 `DisableNativeTools` 生成允许的 builtin tool surface，并通过显式 MCP config 只加载当次 launch 的 MCP。Kimi Wire 的 prompt、steer、cancel、usage、activity 与 generic recovery 继续使用同一 session-bound transport。
- `builtin` 选择解析到 AgentTeam-managed OpenCode，并沿用 OpenCode 当前的 full-access launch policy；builtin rewind capability 跟随 OpenCode `opencode_session_revert`，发布前同样通过 LAN E2E 验收。
- 外部 OpenCode、Claude Code、Codex、Kimi Code 仍可检测和使用，但 Desktop 只负责 detect/use/configure，不再提供 runtime install、upgrade、SSE installer、install command API、launch-time auto-install 或 app-build auto-install。Codex 检测和启动仍要求 `codex app-server` capability；Kimi 检测和启动要求 `kimi --wire` capability。

因此当前服务端判断 rewind eligibility 时，必须按**有效 runtime implementation** 判断，而不是只看”是不是 Codex”。

补充边界说明：

- internal data plane 当前仍以 workspace-scoped `agentId` 为入口，因为 act-as、mention、delivery、channel membership 都锚定在 `Agent` / agent member 上
- 与该入口绑定的 runtime token 仍存于 `agents`。`session_id`、`status/activity`、`activity_detail`、`current_instance_id` 和 `agent_instances` 属于 legacy runtime mirror；目标 contract 不再把它们作为当前 runtime source of truth。daemon-local state 与当前 WS 连接负责 session/resume、running set、activity 和 trajectory，server 只转发 realtime 或保留必要的 ephemeral routing state

Live runtime read-model overlay（As-Is contract）：

- Canonical daemon ownership is defined in `specs/daemon/daemon.md`; Desktop package layering is defined in `specs/daemon/desktop-architecture.md`.
- `internal/api/daemon/protocol.go` owns the server in-memory projection keyed by the authenticated daemon WebSocket connection. It consumes `ready.runningAgents`, `agent:status`, `agent:activity`, and `agent:session` messages without writing those transient fields to TiDB, except for the explicitly documented sparse legacy `agents.session_id` mirror for daemons that do not support local session resume.
- Durable store queries such as `internal/store/app/app_runtime.go`, `internal/store/app/app_agent_detail.go`, and `internal/store/app/agent_messages.go` remain responsible for bounded authorization and identity/machine rows. They may return offline placeholders for live-only fields.
- API handlers that already have bounded result sets overlay the daemon hub projection before converting to DTOs: browser agent list/detail/runtime snapshots under `internal/api/webagent`, machine agent/workspace views under `internal/api/workspace/api_machines.go`, and agent-facing MCP workspace snapshots under `internal/api/agent/api_messages.go`.
- The overlay must not add agents that the durable query did not return, must not widen list/query scope, must not add polling, and must not persist status/activity/current-instance telemetry. If the daemon disconnects, the server-side in-memory projection disappears with that connection and subsequent reads show offline/unknown until reconnect and `ready` replay.

当前实现（按更严格方案落地）：

- 每次 server 准备下发 `agent:start` 时生成新的 `agentAuthToken`，并将其 `SHA-256` 写入 `agents.auth_token_hash`（旧 token 立即失效）
- agent 首次 `start` 前，`agents.auth_token_hash` 允许为空；此时对 workspace-scoped Agent Runtime 的请求一律返回 `401`
- server 在下发 `agent:start` 的 `config.authToken` 时带上该 token；daemon 会把它透传给本机 chat-bridge
- **不提供 machineApiKey 作为 internal API fallback**

理由：

- machine key 是机器级权限，agent token 是 agent 级权限，不能混用
- 当前系统并不缺 fallback；增加 fallback 只会掩盖配置错误
- 去掉 fallback 也更符合“尽早暴露问题”的工程原则

长期记忆接入约束：

- runtime-local recovery memory 仍然是 agent workspace 本地文件：`MEMORY.md`、`state/*`、`kb/*`、`archive/*`。
- 这些文件由 daemon/runtime 与 agent 直接读写，用于 deterministic resume、local knowledge 和历史追溯；它们不是 metadata-backed product surface。
- MeshX managed Memory 是独立的 workspace resource surface。它不替代 runtime-local recovery memory；human visibility/manage 通过 `memory_grants` fail-closed 授权，agent access 则通过 `memory_agent_bindings` 的显式单绑定模型解析。当前 agent 绑定不使用 `memory_grants(subject_type=agent)`，也不提供 workspace-wide fallback access。
- 当前系统不提供 semantic/vector memory 服务，也不暴露 memory MCP 工具。

权限最小化：

- Agent token 只用于 internal agent HTTP API（当前实现包含）：
  - 消息：Agent Runtime 下的 `/send`、`/history`、`/search`、`/messages/:messageId/read-check`、`/workspace`（server 不再提供 `/receive`）
  - Task Board：`/api/v1/workspaces/:workspaceId/agents/:id/runtime/tasks*`（list/get/create/update/set-claim/checkpoint）
- `internal/store/agent` runtime auth 现在会把这条 internal data-plane 身份显式编译成 `AuthorizationRequest{ resource=agent, action=act_as_member }`，并要求 `act_as_member` 只能命中 agent 自己的 member（decision source=`agent_self`）
- Machine token 仅允许：
  - 建立 WS 控制连接
  - 不允许访问 internal agent HTTP API
- machine WS 认证同样显式落到 `machine.connect`（decision source=`machine_self`），避免 machine token 语义继续停留在隐式“查到这台机器就算通过”

### 4.4 数据模型：元数据在 TiDB，冷历史在归档存储（Archive Storage：filesystem / S3）

实现位置（As-Is）：

- TiDB schema：`internal/tidb/bootstrap.sql` + `internal/tidb/migrations/*.sql`
- Release activation 在运行 `agentteam-schema init` 前停止旧 `agentteam-server`，schema
  与 workspace placement gate 全部通过后才启动新 binary；contract migration
  失败时保持服务停止，避免旧 binary 在部分升级的 schema 上继续写入。
- 消息写入与访问原语：`internal/store/app/message_append.go`、`internal/store/app/app_messages.go`、`internal/store/message/append.go` 与 `internal/store/message/scope_store.go`
- 消息冷归档 worker 当前未运行；冷历史读取仍使用 `internal/archive/*`
- 冷归档存储 driver：`internal/archive/*`

#### 4.4.1 先定一个简单模型

统一抽象：

- **scope = channel、DM、reply thread、Task、Service Account 或 Workflow 等产品消息容器**；外部 target 仍保留各产品语义，存储层不再维护并行 legacy thread 拓扑。
- 每个 scope 有自己的单调递增 `seq`。
- scope 的消息默认是 append-only 的，不支持普通修改与删除；严格受限的 rewind 成功后会把目标热 suffix 写成 `rewound` lifecycle tombstone 并 scrub 派生投影。

数据分两层：

- **热层（TiDB）**：
  - 元数据
  - 读状态
  - 最近消息正文（`scope_messages`）
- **冷层（Archive Storage）**：
  - 不再频繁读取的历史归档对象（按 `scope_id + seq` 直接推导 object key；storage driver 可为 filesystem 或 S3）

数据库不再有“永久 `messages` 大表”。

首版最关键的取舍：

- 写入路径必须简单可靠，因此**新消息先写 TiDB**
- 冷历史异步归档到归档存储（filesystem / S3），归档后再从 TiDB 删除
- 这样写入仍然是单事务、强一致、容易调试
- 同时数据库规模可以保持在“热消息窗口”内，而不需要永久承载全部历史正文

#### 4.4.2 主要表（概念级）

> 说明：完整 schema 以 `internal/tidb/bootstrap.sql` + `internal/tidb/migrations/*.sql` 为准；本节只列出理解当前实现所需的关键列/约束/语义。

基础元数据：

- `users`
  - `id`, `email`（PingCAP phone-only 用户可为 `NULL`）, `name`, `display_name`, `avatar_url`, `is_admin`
  - `password_hash`, `email_verified_at`, `mobile`, `mobile_verified_at`, `created_at`, `updated_at`
- `user_email_verification_tokens`
  - `user_id`, `token_hash`, `expires_at`, `consumed_at`, `created_at`
- `user_sessions`（access token）
  - `id`, `user_id`, `session_family_id`, `token_hash`, `expires_at`, `created_at`, `revoked_at`
- `user_refresh_tokens`（refresh token）
  - `id`, `user_id`, `session_family_id`, `token_hash`, `expires_at`, `created_at`, `revoked_at`
- `user_session_families`（二维码登录的移动端会话族）
  - `id`, `user_id`, `client_type`, `installation_id`, `device_name`, `device_platform`, `expires_at`, `created_at`, `revoked_at`
- `user_password_reset_tokens`
  - `user_id`, `token_hash`, `expires_at`, `consumed_at`, `created_at`
- `workspaces`
  - `id`, `organization_id`, `slug`, `name`
  - `organization_id` 是 owning organization 的唯一事实来源；旧 workspace binding 表已由 cleanup migration 删除
  - `latest_message_seq`（workspace 内全局单调递增消息序列）
  - `created_at`, `updated_at`
- `workspace_invites`
  - `workspace_id`, `email`, `role=member`
  - `token_hash`, `created_by_user_id`, `created_at`, `expires_at`, `accepted_at`, `revoked_at`
- `members`
  - `id`：稳定 `member.id`，shortid 风格字符串；对 human 它直接就是公开协议标识，对 agent / service 它是隐藏内部关联键
  - `workspace_id`
  - `type = human | agent | service`
  - `display_name`
  - `user_id`（human only）
  - `workspace_role`（human only）= `owner | admin | member`
  - 关键约束（以 `internal/tidb/bootstrap.sql` + `internal/tidb/migrations/*.sql` 为准）：
    - `type` 仅允许 `human|agent|service`
    - human 必须有 `user_id` 且必须有 `workspace_role IN ('owner','admin','member')`
    - agent / service 必须 `user_id IS NULL` 且 `workspace_role IS NULL`
    - `members_workspace_human_user_unique`：同一 workspace 内 `(workspace_id, user_id)` 仅对 human 做唯一约束
- `channel_sidebar_preferences`
  - 以 `(workspace_id, member_id)` 为主键保存 Human 的个人频道分类聚合状态；`member_id` 通过 human member 唯一映射当前 workspace 中的 user，确保跨客户端同步但不跨成员共享
  - `state_json` 原子保存有序分类、名称、图标、频道归属和折叠状态；Browser 通过同一个 `/api/v1/workspaces/:workspaceId/sidebar/channel-sections` URL 执行 GET/PUT，未部署或暂时不可用时只使用按 workspace+user 隔离的 localStorage 缓存
Scope message model：

- `scopes`
  - `id`, `workspace_id`, `type = channel | dm | thread | task | service_account | workflow`
  - `parent_scope_id`, `root_message_id`, `root_message_short_id`, `identity_key`
  - `latest_seq`, `latest_global_seq`, `latest_message_at`, `archived_until_seq`, `visible_message_count`, `visible_last_message_at`, `status`, `created_by_member_id`, `created_at`, `updated_at`, `archived_at`
  - channel、DM、reply thread 都是 first-class scope rows；reply-thread scope 指向 parent scope 与 root message；DM 唯一性通过 canonical `identity_key` 表达。
- `scope_channel_profiles`
  - `scope_id`, `slug`, `display_name`, `description`, `visibility`, `subsystem`, `owner_member_id`, `is_all_channel`, `created_at`, `updated_at`
  - 只有 `type='channel'` 的 scope 有 profile；`#all`、ordinary channel governance 与 public/private discover metadata 都在这里。`subsystem` 默认为 `im`，`kb` 为历史兼容的隐藏 agent-thread/session carrier channel 值，`project` 预留给 Project 内部讨论 carrier；普通频道列表/发现/配额默认只读取 `im`。
- `scope_memberships`
  - `scope_id`, `member_id`, `visibility`, `owner_member_id`, `last_read_seq`, `last_read_global_seq`, `owner_context_dismissed_through_global_seq`, `effective_from_seq`, `effective_to_seq`, `removed_at`, `archived_at`, `updated_at`
  - owns dense readability, member/participant projection, read cursors, private membership intervals, and owner-context dismiss cursor.
- `scope_notification_settings`
  - `scope_id`, `member_id`, `state = active | quiet | mute | auto-wake | no-wake`, `created_at`, `updated_at`
  - owns current wake/read notification preference; per-message execution truth stays in fanout payload/outbox rather than notification history rows.
- `scope_messages`
  - `id`, `workspace_id`, `scope_id`, `seq`, `workspace_global_seq`, `author_member_id`, `author_type`, `client_message_id`, `message_type`, `content`, `metadata`, `created_at`, `updated_at`
  - stores canonical live transcript rows for explicitly supported product scopes, including channel / DM / reply-thread and Workflow Builder history; owner-private execution does not create a separate user-visible transcript.
- `scope_share_cards`
  - `id`, `workspace_id`, `scope_id`, `message_id`, `source_scope_id`, `source_message_id`, `target_scope_id`, `created_by_member_id`, `revoked_at`, `revoked_by_member_id`, `payload`, `created_at`, `updated_at`
  - share-card action state belongs to the owning `channel_share_card` message; lifecycle and authorization stay out of message metadata.
- `scope_activity_events`
  - `id`, `workspace_id`, `scope_id`, `activity_type`, `activity_ref_type`, `activity_ref_id`, `activity_global_seq`, `source_message_id`, `actor_member_id`, `reason`, `payload`, `created_at`, `suppressed_at`
  - durable fact table only for non-message activities such as reaction signals and task typed activity projections; message-backed mention / explicit / broadcast reasons stay in fanout payload and outbox status. Task typed activities are written through `internal/store/tasktimeline` rather than taskboard/app feature-local inserts.
- `scope_message_sources`
  - source refs for concrete `owner_quote` message provenance and source attribution; current parent-target/private-context runtime does not write separate task-owned source entries, second transcript copy rows, or publication mirrors
- `scope_message_reactions` / `scope_activity_events` reaction rows
  - `scope_message_reactions` carries current reaction state for a scope message; reaction activities use `scope_activity_events.activity_ref_type = 'reaction'`, so reactions can generate agent-facing daemon delivery signals / diagnostics without a separate delivery table

读状态：

- `scope_memberships`
  - `last_read_seq`, `last_read_global_seq`, and `owner_context_dismissed_through_global_seq` are the canonical live read-state fields for scope reads.

activity 与 rewind 状态：

- `scope_activity_events`
  - `id`, `source_message_id`, `scope_id`, `activity_global_seq`
  - `activity_type`, `activity_ref_type`, `activity_ref_id`, `reason`, `created_at`, `suppressed_at`
  - 作为 reaction 等非消息 activity 和 task typed activity projection 的 durable fact；task typed activity 通过 `internal/store/tasktimeline` 统一写入，message-backed mention / explicit wake 只影响 outbox fanout reason。普通 channel / DM / reply-thread 的 dense unread/readability 使用 `scope_memberships` cursor 与 current `scope_notification_settings` 偏好；每条消息的 agent execution truth 使用 `scope_fanout_outbox`。
- `message_recovery_points`
  - `workspace_id`, `message_id`, `agent_id`
  - `scope_id`, `recovery_strategy`, `recovery_ref`, `prepared_at`
  - 只在消息实际会通过 rewind-capable runtime 投递给 agent，且消息仍位于 hot history 时写入
- `rewind_operations`
  - `id`, `workspace_id`, `scope_id`, `message_id`, `agent_ids`, `status`, `created_at`, `updated_at`
  - `status = preparing | repair_required | completed | failed`
  - 用于串行化单个会话面的 rewind，并在 restore 成功但 DB apply 失败时留下 repair ledger

机器与 agent：

- `machines`
  - `id`, `workspace_id`, `created_by_user_id`
  - `name`
  - `api_key`
  - `hostname`, `os`, `daemon_version`, `runtimes`（last-known diagnostic metadata；runtime decisions use the current daemon connection）
  - `status = active | revoked`
  - `connected_at`, `disconnected_at`（legacy presence mirror；machine online/offline must come from daemon WS presence, not these columns）
  - `created_at`, `updated_at`
- `agents`
  - `id`, `workspace_id`, `machine_id`, `member_id`, `name`, runtime settings
  - `UNIQUE(workspace_id, name)`
- `agent_role_bindings`
  - binds each Agent to a Role version and active identity document
- `role_package_manifest_items`
  - canonical package manifest for identity, skills, knowledge, KB, and Memory grants

Role never replaces `members` / `agents` as the message identity. Materialization reads only canonical bindings and manifest items.

消息热层：

- `scope_messages`
  - `id`, `workspace_id`, `scope_id`
  - `seq`（scope 内序列），`workspace_global_seq`（workspace 内全局序列）
  - `author_member_id`, `author_type`, `message_type`, `content`, `metadata`, `created_at`, `updated_at`
  - `UNIQUE(scope_id, seq)`
  - `UNIQUE(workspace_id, workspace_global_seq)`
  - `metadata`：bounded JSONB（当前上限 `16KiB`），用于附加 machine-readable sender/event metadata，例如 App sender kind/slug
  - `content` 是 Markdown 源文本；正文内附件用 `attachment://<attachmentId>` 引用。引用在 `content` 中出现的位置和顺序是排版事实来源，不依赖 attachment 查询顺序
- `attachments`
  - `id`, `workspace_id`, `scope_id`, `message_id`
  - `uploaded_by_member_id`
  - `filename`, `content_type`, `size_bytes`, `blob`, `created_at`
  - 备注：当前实现直接把二进制内容存 TiDB（便于最小化依赖与实现复杂度）；message attachment 通过 `scope_id + message_id` 绑定到 scope message，task discussion attachment 绑定到 task scope 的 `scope_messages(message_type='task_discussion')`，未 claim 上传保留 `scope_id` 以便 GC。发送请求中的 `attachmentIds` 负责 claim 集合与权限边界，不定义正文内的展示顺序；正文引用只有在 ID 属于当前 message sidecar 时才可解析。若未来需要保留历史附件，应优先迁移到对象存储（S3/FS）并仅在 DB 存元数据/引用。
  - 备注：当前实现会通过后台 GC 定期删除未 claim 附件（`message_id IS NULL` 且 `created_at < now - 24h`，实现：`internal/app/app.go` + `internal/store/attachment_gc.go`）；因此“弃发附件”不会无限占用 TiDB，但上传后若长时间未发送，需要重新上传

序列字段（两套 cursor）：

- desktop/internal：以 scope-local `seq` 为主；普通 channel / DM / reply-thread 的 human unread 由 `scope_memberships` cursor 推导；agent runtime delivery 由写入时 routing plan + `scope_fanout_outbox` payload/status 下发到 daemon，本地 continuation inbox 保存 daemon 已接受但尚未在 runtime surface 的 payload
- web/app：以 `workspace_global_seq` 为准（`workspaces.latest_message_seq` / `scope_messages.workspace_global_seq`）
  - 写入时由 `workspaces.latest_message_seq` 在事务内自增分配，保证 `UNIQUE(workspace_id, workspace_global_seq)`
  - `GET /api/messages/sync?since_seq=...` 与人类端未读计数依赖 `last_read_global_seq`

命名变化的关键点：

- 原 `actors` 改为 `members`
- human 的原 `actor.name` 语义改为 `members.id`
- agent 的原 `actor.name` 在 protocol / target 语义上改为 `agents.name`；默认 human-visible label 改由 `members.display_name` 承担，`agents.member_id` 退回隐藏 relation key
- `sender_name` / DM `channel_name` 对 internal 协议来说，human 传 `member.id`，agent 传 `agents.name`，service 传安装态公开 handle（当前 App v1 即 slug）

workspace 角色也刻意不单独再起一张 `workspace_memberships` 表，而是直接落在 human member 行上的 `workspace_role`。

原因是：

- 当前模型里，一个 user 在一个 workspace 下恰好只有一个 human member
- public API 的权限判断最终也总要先定位这个 human member
- 因此把 `workspace_role` 直接落在 human member 行上，能让权限判断有单一事实来源，同时不把 agent 运行态混进来

这里刻意不采用“只有 `members` 一张表，再加 `is_agent` 布尔字段”的方案。

原因不是它做不到，而是它并不会真的让模型更简单：

- `members` 表达的是**消息身份**：谁在发言、被 @mention、被用作 DM 对端
- `agents` 表达的是**运行实体**：它属于哪台 machine、用什么 runtime、当前 session / status / activity 是什么

因此首版采用：

- `members.type = human | agent | service`：把成员类型表达清楚，而不是塞进语义更弱的 `is_agent` 布尔值
- `agents.member_id`：让运行实体单向指向自己的消息身份
- `members` **不反向存 `agent_id`**：避免双向引用和重复约束

新增的 service sender 也遵循同一原则：core 只把它当作 `members.type = service` 的消息身份；App 安装、manifest、grant、subscription 等附加语义留在 `app_*` 表，不回灌成新的 core principal。


#### 4.4.3 为什么这个模型能明显减轻 DB 负担

因为首版几乎所有核心读写都只需要这些元数据，不需要扫完整历史：

- 未读：
  - desktop/internal：普通 scope human unread 以 member/scope cursor 为边界；non-message activity 保留为 durable fact / diagnostics / direct fanout evidence，不再通过 server `/receive` 推进 agent activity cursor
  - web/app：基于 `scope_messages.workspace_global_seq` 与 `scope_memberships.last_read_global_seq` 增量计算（热窗口内）
- 最新页：只查 `scope_messages` 末尾几十条
- agent 当前事件投递：写入时根据 message routing plan 计算 durable/wake recipients，事务提交后由 `scope_fanout_outbox` worker 下发 `agent:start` / `agent:deliver`；daemon-local continuation inbox 保存并自动递交已被 daemon 接受的 pending payload
- 翻冷历史：只用 `scopes.archived_until_seq` 判断是否落到冷层；落到冷层后，由 `scope_id + seq` 直接推导要读哪个归档对象（chunk）
- 冷层按随机 `message_id` 或 reply-thread short id 定位时不能从 seq 推导入口；当前用窄表 `scope_archived_messages` 存 `message_id / message_short_id -> seq`，命中后按固定 chunk 策略派生对象并校验内容

因此数据库的角色是：

- **活跃 tail 的读写引擎**
- **边界元数据与读状态存储**
- **不是冷归档对象内容目录，也不是永久历史文件柜**；仅保留随机 id / short id 这类无法由 `scope_id + seq` 推导的窄定位索引

#### 4.4.4 热层边界与消息尺寸约束（保持简单）

首版只需要：

- `scope_messages` 的两组唯一序列：
  - `UNIQUE(scope_id, seq)`：支撑 desktop/internal 的按 scope 历史分页与归档分块
  - `UNIQUE(workspace_id, workspace_global_seq)`：支撑 web/app 的跨 scope 增量同步（`/api/messages/sync`）
- `scope_archived_messages(scope_id, message_id)` 与 `(scope_id, message_short_id, message_id)`：支撑已归档消息按随机 message id / reply-thread short id 定位到单个 chunk；读取内容仍以归档对象为准
- `scopes.latest_message_at`：支撑 channel / DM / reply-thread 列表与 inbox 排序
- `scopes.archived_until_seq`：支撑冷热边界判断
- `scope_memberships(scope_id, member_id)` 主键：支撑 human unread/read-state（含 `last_read_seq` 与 `last_read_global_seq`）；agent runtime delivery 不再从该 cursor 派生 `/receive` backlog

当前实现依赖的关键索引（非穷举，完整见迁移文件）：

- `scope_messages` 的 `(scope_id, seq)` 索引：scope 维度读取最近消息（最新页与向前翻页）
- `scope_messages` 的 `(workspace_id, workspace_global_seq)` 索引：`/api/messages/sync` 增量拉取
- `tasks_channel_status_task_number_idx`：task board 列表（按 `status` 过滤）

首版把单条消息大小限制死（当前实现：写入路径已强制校验；按字节长度计）：

- `message.max_bytes = 32 KiB`（实现：`internal/store/privateagent/conversation_write.go`）

理由：

- message `content` 仍是有界 text 字段，只承载 Markdown 源文本而不引入 rich-text AST；web/app 可在其中用 `attachment://` 引用 sidecar 附件
- 固定大小上限能显著降低异常大消息把热层、归档对象和 agent prompt 一起拖垮的风险
- 也让后面的固定消息块归档策略保持可控

同时建议对附件做硬限制（当前实现已落地在 public API 层）：

- `attachments.max_files_per_message = 10`
- `attachments.max_bytes_per_file = 20 MiB`
  - 实现：`internal/api/media/api_attachments.go`（web/app 上传）
- 正文媒体表示：图片持久化为 Markdown image；视频/音频持久化为标准 Markdown attachment link，Web 根据当前 message sidecar 的 `content_type` 升级为 `<video controls playsInline>` / `<audio controls>`。不支持媒体升级的旧客户端仍可把同一引用作为鉴权下载链接，未被正文引用的旧附件继续走消息底部 sidecar
- HTML 附件也持久化为标准 `[file.html](<attachment://id>)`，不新增可执行消息类型。Web 优先按 sidecar `content_type = text/html`、再以 `.html` 文件名 fallback 识别，在引用位置渲染有尺寸上限的小型静态预览；点击预览打开使用同一安全文档的放大视图，识别失败、净化失败或旧客户端均保持下载链接
- 第一版静态 HTML renderer 必须先净化文档，再放入不授予 `allow-scripts`、`allow-forms`、`allow-popups`、`allow-top-navigation` 或 `allow-same-origin` 的 sandbox iframe。净化层移除 script、inline event handler、form、meta refresh、可导航链接、nested frame/object/embed 等 active content；iframe 文档 CSP 使用 `default-src 'none'`，仅按需要允许内联样式和 `data:`/本地 Blob 图片，从而不向外网加载资源。缩略与放大视图必须复用同一净化产物，不能在放大时回退到未隔离的原始 HTML
- 交互式 JavaScript 原型不得通过消息应用 origin 的 Blob URL、`srcdoc` 或放宽 sandbox flag 来实现。若未来支持可执行预览，必须使用与 AgentTeam cookie/storage 隔离的独立 preview origin，并为短期访问凭证、严格 CSP、资源代理与受限 `postMessage` 协议建立独立安全 contract
- 当前 Web 播放视频时通过鉴权下载接口读取完整 payload，再创建 Blob URL；即使播放器使用 `preload="metadata"`，传输仍不是 Range streaming。该路径受 `20 MiB` 单文件上限保护；如果以后放宽大小或要求首帧/seek，下载接口与授权模型必须支持短期媒体 URL、`Range` 请求和 `206 Partial Content`
- internal agent API 的 `upload_file` 当前收敛为：单文件最大 `5MB`
  - 这个值来自当前 agent toolchain 与历史外部 artifact 的共同保守约束，不是默认外部基线
  - 实现：`internal/api/agent/routes.go` + `internal/api/agent/api_*.go`（见 4.7.3）

当前实现已**临时关闭** public 历史窗口限制与自动消息归档：

- `/api/messages/*` 当前读取 `scope_messages` 的全部 human-visible 消息，不再按 `created_at` 做 `AGENTTEAM_MESSAGES_RETENTION_DAYS` 时间窗过滤
- `GET /api/messages/channel/:channelId` 当前不再返回 `historyLimited=true`
- Agent Runtime 的 history、search、read-check 仍保持 hot/cold 读路径能力，但新的部署不会再自动把老消息从 `scope_messages` 挪到 cold archive；server `/receive` 已删除
- `GET /api/public/config` 仍保留稳定 wire 字段 `messageHistoryDays`，但当前返回 `0` 表示“未启用 public 历史窗口限制”
- `AGENTTEAM_MESSAGES_RETENTION_DAYS`（旧名 `AGENTTEAM_MESSAGE_HISTORY_DAYS` 仍作兼容别名读取）当前仅作占位保留，供后续产品策略收敛时重新定义；它暂时不再控制 public 历史可见窗口，也不再驱动自动归档

之所以这样做，是因为近期用户已经反馈在 web/public 侧找不到旧消息；在新的产品策略明确前，系统短期内优先选择“消息持续留在热表并保持可见”。

首版**不建议一开始就做**：

- 大量二级索引
- 时间分区表
- 冷层对象目录 / segment 索引表
- 全文搜索索引

原因：

- `scope_messages` 本来就应当是有上界的“热窗口”
- 一旦引入 `message_segments` 这种 per-object 元数据表，冷归档读取路径、备份恢复路径、归档幂等路径都会重新依赖数据库内容
- `scope_archived_messages` 是随机 `message_id` / short id 定位的窄索引，不记录对象边界，也不替代按 `scope_id + seq` 读取 chunk 的主路径
- 如果未来观测到 `scope_messages` 仍偏大，第一优先动作是收紧热尾窗口或消息大小上限，而不是增加元数据表

### 4.5 冷消息归档对象（Archive Storage）设计

实现位置（As-Is）：

- 冷归档对象格式/编码/校验：`internal/archive/archive.go`
- Filesystem driver：`internal/archive/filesystem.go`
- S3 driver：`internal/archive/s3.go`
- 消息归档 worker 当前已禁用；冷归档对象仍由 `internal/archive` 定义，历史 fixture 和兼容读路径通过 `archive.Storage` 访问
- 现有对象格式只覆盖消息文本，不具备 message attachment manifest/blob lifecycle。重新启用 worker 的硬前置条件是：归档与恢复必须保留正文 `attachment://` 引用对应的稳定 attachment ID、文件名、MIME、大小和可下载 blob，并在热消息删除后继续执行同一 scope/message 鉴权语义

#### 4.5.1 归档对象格式与命名

冷历史建议使用：

- **格式**：NDJSON
- **压缩**：zstd
- **内容**：按 `seq` 升序，一行一条消息

每条消息最小字段：

```json
{
  "id": "00000001d34db33f...",
  "seq": 101,
  "sender_type": "human",
  "sender_name": "m8x2ra",
  "content": "hi",
  "timestamp": "2026-03-09T12:34:56.789Z"
}
```

说明：

- 现有冷归档对象存上述字段（含 `id`），但不包含 `global_seq`/附件信息；因此当前 cold history 读取不会返回附件。这个缺口不能在归档 worker 重新启用后继续存在
- 归档能力恢复前必须同时定义 message attachment manifest 与 blob lifecycle：manifest 至少保留正文引用所需的 `id`、`filename`、`content_type`、`size_bytes` 和 durable blob locator；blob 只有在消息及其归档保留期结束后才能回收，不能仅因对应 `scope_messages` 热行被删除而回收
- 若发送者是 service，则 `sender_type = "service"`，`sender_name` 存安装态公开 handle；人类可读名称应由上层 display projection 提供。

对象 key 必须是**纯可推导**的：

- S3 driver：`s3://<bucket>/<prefix>/workspaces/<workspaceId>/scopes/<scopeId>/chunks/<chunkStartSeq>.ndjson.zst`
  - `prefix` 来自 `AGENTTEAM_ARCHIVE_S3_PREFIX`（默认 `agentteam/archive`；可为空）
- filesystem driver：`<filesystemRoot>/workspaces/<workspaceId>/scopes/<scopeId>/chunks/<chunkStartSeq>.ndjson.zst`
  - `filesystemRoot` 来自 `AGENTTEAM_ARCHIVE_FILESYSTEM_ROOT`（默认 `.data/archive`）

driver 选择由 `AGENTTEAM_ARCHIVE_DRIVER` 控制（`filesystem`/`s3`；默认 `filesystem`）。当 driver 为 `s3` 时，还需要配置 `AGENTTEAM_ARCHIVE_S3_REGION` 与 `AGENTTEAM_ARCHIVE_S3_BUCKET`。

其中：

- `chunkStartSeq = ((seq - 1) / archive.ChunkMessageCount) * archive.ChunkMessageCount + 1`
- `chunkEndSeq = chunkStartSeq + archive.ChunkMessageCount - 1`

每个归档对象只存一个完整的固定消息块；也就是说，给定某个 `seq`，服务端无需查数据库里的段索引，就能直接算出对应的 object key。若入口只有随机 `message_id` 或 reply-thread short id，服务端先查 `scope_archived_messages` 得到 `seq`，再按固定 chunk 策略读取该对象并校验内容。

这样设计的好处：

- 按 `seq` 读冷归档（filesystem/S3）不再取决于 DB 里的逐段元数据
- 备份恢复更简单：消息对象本身是自描述且可推导的；启用附件归档后，恢复集还必须包含对象引用的 attachment manifest 与 blob，不能只备份 `archived_until_seq` 和热数据
- 即使极端情况下 DB 边界丢失，也可以用离线恢复工具通过列举 S3 prefix / 本地目录重新审计有哪些归档块

#### 4.5.2 固定消息块 + 固定热尾策略

这次明确**不采用** “按 size / time 动态滚段 + DB 记录每段元数据” 的方案。

原因很直接：

- 动态滚段意味着每个冷对象的边界都不是公式可推导的
- 一旦边界不可推导，就必须引入额外元数据层
- 这正是 `message_segments` 带来的复杂度来源

因此首版采用两个全局固定参数：

- `archive.ChunkMessageCount = 1000`（固定常量，见 `internal/archive/archive.go`）
- 重新启用写侧归档时至少保留 `2000` 条热消息；当前 worker
  未运行，因此代码中不保留未被执行路径消费的策略常量

行为规则：

- 每个 scope 的冷层只由若干个**完整的固定消息块**组成
- 当前热区间恒为：`(archived_until_seq, latest_seq]`
- 只有当某个最老块已经完整闭合，且归档后仍能在 DB 中保留至少
  `2000` 条热消息时，才允许把它归档到冷归档存储
- 低频 scope 可能长期没有凑够一个可归档块；这是可接受的，因为低频 scope 本来就不是数据库压力来源

这个策略的核心收益：

- 去掉 `message_segments`
- 去掉“按 seq 读冷归档先查 DB segment 索引”这一步；随机 id / short id lookup 只保留窄定位索引
- 去掉“恢复时必须确保 segment metadata 与归档对象一一对应”这类脆弱关系

#### 4.5.3 重新启用归档时的流程约束

消息冷归档 worker 当前未运行。重新启用时，因为 TiDB 与归档存储（filesystem/S3）之间没有跨系统事务，归档流程必须支持 retry；在去掉 `message_segments` 后，推荐流程如下。

推荐流程：

1. 归档器挑一个 scope，要求同时满足：
   - 移走一个固定块后仍保留产品规定的热消息尾部
   - 目标块已落在当前 public 历史窗口之外：`MAX(created_at WHERE seq BETWEEN seq_from AND seq_to) < now - AGENTTEAM_MESSAGES_RETENTION_DAYS days`
     - 当前实现用 `AGENTTEAM_MESSAGES_RETENTION_DAYS` 同时控制 `/api/messages/*` 的可见历史窗口与归档时机
     - 归档时不能只看 `seq_to` 的 `created_at`：`created_at` 可能非单调；用块内 `MAX(created_at)` 才能保证该 chunk 的所有消息都在 cutoff 之外
2. 计算候选块：
   - `seq_from = archived_until_seq + 1`，并校验它是 `archive.ChunkMessageCount` 的合法块起点
   - `seq_to = seq_from + archive.ChunkMessageCount - 1`
3. 从 `scope_messages` 流式读取该范围，编码为 `.ndjson.zst`；重新启用附件消息归档时，还必须收集每条消息的 attachment manifest，并确保对应 blob 已进入可随冷消息共同恢复的 durable lifecycle
4. 写入到其**确定性的最终 key**（create-if-absent；若对象已存在则必须与本次内容一致，否则视为冲突并报错）；含附件的块只有在 message object、manifest 和 blob 都可验证后才算写入成功
5. 写入成功后，开启一个 TiDB 事务：
   - `INSERT` / refresh `scope_archived_messages` 中该 chunk 的 `message_id / message_short_id -> seq` 定位索引
   - `DELETE FROM scope_messages` 中该 scope 映射的 hot rows（按 `seq BETWEEN seq_from AND seq_to` 精确删除）；此动作不得触发仍处于冷消息保留期内的 attachment blob 回收
   - `UPDATE scopes SET archived_until_seq = seq_to`
6. 事务提交成功后，该完整消息块正式从热层移到冷层

为什么这仍比 `message_segments` 更简单：

- 不需要记录冷对象边界、URI、大小或校验和；chunk key 仍由公式推导
- 窄定位索引只把随机 id 映射到可验证的 `seq`，不作为消息内容 source of truth
- 只要 `archived_until_seq` 没推进，读路径就不会把这个块当成已归档完成
- 如果在“写入成功、DB 事务未提交”之间崩溃，下次可以对同一个 key 重试：对象不存在则写入；对象已存在且内容一致则视为成功；内容不一致则必须报错暴露问题

#### 4.5.4 冷读优化：本地归档缓存（默认先不做）

首版默认**不把本地磁盘缓存作为基础架构的一部分**。

理由：

- 固定消息块已经显著收敛了冷读复杂度
- 没有真实观测之前，不值得再引入一套磁盘 LRU、淘汰策略和缓存命中监控
- 对当前项目来说，先把“固定块 + 纯可推导 object key + 简单归档器”做扎实，比再加一层缓存更重要

如果未来观测到同一批冷对象被反复读取，再加一个**可随时清空的本地 LRU 缓存**即可；它应被视为二阶段性能优化，而不是首版必选项。

### 4.6 控制面协议：Daemon WebSocket

实现位置（As-Is）：

- HTTP Upgrade / 鉴权：`internal/daemonws/http.go`
- 连接生命周期 / 心跳 / 协议处理：`internal/daemonws/hub.go`
- 协议校验与关键行为测试：`internal/daemonws/hub_protocol_validation_test.go`、`internal/daemonws/hub_autowake_integration_test.go`

**Endpoint**

- `GET /api/v1/workspaces/:workspaceId/daemon/connect?key=<machineApiKey>`（Upgrade 到 WS）

**连接建立后的握手**

1. server 认证 `machineApiKey`，找到 `machineId` / `workspaceId`
2. server 绑定该 machine 的 WS 连接（内存 `machineId -> conn`）
3. daemon 发送 `ready`（含 `runtimes` / `hostname` / `os` / `daemonVersion` / `runningAgents`；`capabilities` 字段当前不持久化，当前 capability 包含 `agent:permission-mode` 用于声明 daemon 能执行 agent runtime 权限模式，以及 `agent:maas-direct` 用于声明 daemon 能把 hosted MaaS 配置改写到本地 loopback bridge 并直接转发到 `agentteam-maas`）
4. server 更新当前连接内存，emit machine/daemon online realtime，并只在 hostname / OS / daemonVersion / last-known runtime metadata 发生变化时才考虑低频持久化；`ready.runningAgents` 不写 `agents` / `agent_instances`，machine online/offline 不写 `connected_at` / `disconnected_at`

**server 定时 ping（当前实现）**

- server 每 `15s` 发送一次 WebSocket `Ping` control frame（不是 JSON `{"type":"ping"}`）
- server 以 WebSocket `Pong` control frame 或任何应用层消息更新 read deadline（当前 pong wait 为 `45s`）
- daemon 也会发送应用层 `{ "type": "pong" }` JSON 消息；server 支持并同样更新 read deadline

**消息类型与 payload**

- 当前 WS 消息形状已由 AgentTeam 自己的 daemon/server contract 定义，相关回归检查应直接基于本仓库实现与测试
- `agent:start` 负载包含：
  - `config.serverUrl`
  - `config.authToken`
  - `config.sessionId`：legacy 字段；已上报 `agent:local-session-resume` 的 daemon 不再由 server 从 TiDB 读取旧 session id，daemon-local state 决定 resume 或 fresh start。未上报该 capability 的旧 daemon 可继续接收 server 维护的 legacy `agents.session_id` mirror。
  - `config.name`：这里传 **agent 对应的公开 `agents.name`**
  - `config.displayName`
  - `config.description`：直接使用 `agents.description`（不做 server 侧 prompt 注入；保持 server 侧行为简单明确）
  - `config.permissionMode`：用户可选的 agent runtime 权限模式当前只有 `full_access`；历史 `agent_workspace_write` / `restricted` 值仅作滚动升级兼容并统一归一化为 `full_access`，不再表示 workspace-only sandbox。内部 tool restriction 由 launch-scoped `DisableNativeTools` 表达，不是第二个用户 permission mode；server-hosted Knowledge Q&A 不使用 daemon runtime policy
  - direct agentteam-maas bridge：server 只向已上报 `agent:maas-direct` capability 且已配置 `AGENTTEAM_MAAS_RUNTIME_BASE_URL` 的 daemon 下发 `credentialKind=maas_capability` 配置；未满足条件的 daemon 保持 legacy projected key path

##### direct agentteam-maas bridge / MaaS（当前实现）

- desktop 侧 loopback bridge 负责把 runtime 发往 hosted MaaS 的请求改写到本地进程级 bridge，并按 agent runtime 会话向 AgentTeam 签发/刷新 MaaS capability：
  - session issue / refresh client：`desktop/internal/channel/agentteam/llm_proxy_sessions.go`
  - 本地 bridge：`desktop/internal/agent/agentteam_llm_bridge.go`
  - runtime launch 接线：`desktop/internal/dispatcher/process_launch.go`
- server 侧只保留 AgentTeam-owned control-plane 集成：
  - `/api/v1/workspaces/:workspaceId/agents/:agentId/runtime/llm-proxy-session`：`internal/api/agent/api_llm_proxy_sessions.go`
  - MaaS runtime capability 签发/投影：`internal/api/agent/maas_llm_proxy_sessions.go` + `internal/maas/capability/*`，由 `credentialKind=maas_capability` 显式触发，并通过 `agentteam-maas` 的 `/internal/maas/v1/authority-projections/{id}` 推送本地 authority projection
  - authority projection scope read：`internal/store/agent/maas_llm_proxy_scope.go`
  - canonical AgentTeam workspace LLM proxy `/api/v1/workspaces/:workspaceId/llm/*`：`internal/api/llm/*`；旧 `/api/llm/*` 仅是 Gateway compatibility rewrite，不是 Workspace Server 当前路由
  - hosted credit billing browser/API integration：`internal/api/billing/*` + `internal/billing/alipay/*`；AgentTeam 只负责 browser session/org authorization、Alipay page-pay/notify/query，以及调用 `agentteam-maas` `/internal/maas/v1/billing/*`
  - hosted model onboarding/API integration：`internal/maas/availability/*` + `internal/api/org/api_hosted_models.go` + `internal/api/workspace/api_selectable_models.go` + `internal/service/maas/*`；AgentTeam 读取 `agentteam-maas` explicit entitlement/balance/inventory facts，持有 workspace hosted authorization 和 picker policy，并在 agent save、runtime start、MaaS capability issue 前重新校验 `agentteam_hosted`
  - server wiring：`internal/server/wire.go`
- 当前 rollout 边界：
  - new direct-MaaS daemon requests do not send hosted generation traffic to AgentTeam `/api/v1/workspaces/:workspaceId/llm/v1`
  - AgentTeam pushes authority projection before returning a direct MaaS capability, and projection push failures surface as the `projection_push` phase
  - hosted model catalog、route、entitlement、preflight、usage event、reservation、credit ledger、payment order/event storage、provider credential materialization、request status/recovery 均由 `agentteam-maas` spec 与实现持有
  - canonical AgentTeam LLM proxy 不创建 hosted MaaS resolver facts；旧 `/api/llm/*` compatibility 由 Gateway 持有，只在 workspace route header 可校验时改写，并在所有受支持 daemon/Desktop/CLI 已升级且旧路由流量归零后删除；AgentTeam `/api/billing/*` 是 central/control API，且不直接写 `agentteam-maas` payment/ledger tables

#### 4.6.1 控制面消息清单（必须支持）

通用约定：

- 所有**应用层 JSON** WS 消息都必须包含顶层字段 `type`
- 心跳使用 WebSocket control frame `Ping` / `Pong`（不包含 JSON envelope）

server → daemon：

- `agent:start`
- `agent:stop`
- `agent:sleep`
- `agent:deliver`
- `agent:workspace:list`
- `agent:workspace:read`
- `machine:workspace:scan`
- `machine:workspace:delete`
- `agent:reset-workspace`

daemon → server：

- `ready`
- `pong`
- `agent:status`
- `agent:activity`
- `agent:session`
- `agent:trajectory`
- `agent:deliver:ack`
- `agent:workspace:file_tree`
- `agent:workspace:file_content`
- `machine:workspace:scan_result`
- `machine:workspace:delete_result`

#### 4.6.2 展示状态与自动唤醒策略

当前实现暴露出两个需要保守处理的现象：

- server 下发 `agent:sleep` 后，daemon 不会可靠地主动回一个 `status=sleeping`
- `status/activity` 本身只是 daemon 的最佳努力遥测，不适合作为权限或消息投递的关键判据

因此服务端需要把两件事彻底分开：

- daemon telemetry (`agent:status` / `agent:activity` / `agent:session` / `agent:trajectory`)：只给 realtime UI、调试和当前连接上的 best-effort routing hint 使用；server 可以直接转发到 browser WS，或短期放在进程内 ephemeral state，但不得写 `agents.status/activity/activity_detail/current_instance_id` 或 `agent_instances`。`agents.session_id` 只允许作为未上报 `agent:local-session-resume` 的旧 daemon 兼容 mirror，并且只在 session 值变化时写入。
- runtime telemetry 的权限边界以 machine owner 为准：workspace 成员可以收到不含 `currentInstance` 的粗粒度 `agent:status`，但 `agent:activity`、`agent:session`、`agent:trajectory` 以及 refresh/list 响应中的 activity detail、session id、current instance 只定向给 owner；fresh socket bootstrap 也必须复用同一 owner 边界。
- `POST /api/agents/runtime-snapshots` 是显式 agentId bounded 的 UI refresh path。持久层只校验 workspace/member 可见性并返回 durable agent/machine binding、auto-wake intent 和默认 inactive/offline runtime view；API 层再从当前 daemon WebSocket hub 内存合并 machine online、agent running 和 session id。该路径不得读取 legacy presence mirror、`agents.current_instance_id` 或 `agent_instances` 来恢复 live state。
- `agents.auto_wake_enabled`：服务端拥有的唤醒策略位；决定“新消息到来时是否允许自动拉起 agent”

推荐规则：

- `start`（`POST /api/agents/:agentId/start`）：置 `auto_wake_enabled=true`，并 best-effort 下发 `agent:start`
- `stop`（`POST /api/agents/:agentId/stop`）：置 `auto_wake_enabled=false`，并 best-effort 下发 `agent:stop`
- `sleep`（`agent:sleep`）：协议层支持，但当前 public API 未暴露该动作；daemon 即使上报 `status=sleeping`，server 也不应仅因“还存在 unread backlog”再次自动 `agent:start`
- internal `/send` / `/history` / `/search` / read-check 的鉴权与可写性都不依赖最新 `status/activity`
- 即使 agent 最近显示为 `inactive`，只要带着合法 `agentAuthToken` 调用 internal API，server 仍按普通已鉴权 agent 处理
- MaaS / LLM proxy readiness 不能用 `agents.session_id != ''` 判断。需要 runtime readiness 时，应使用当前 daemon 连接上的 explicit ready/lease state；当前单 server 可用进程内 TTL，未来多 server 使用 Redis/TTL，而不是 TiDB
- message routing / delivery diagnostics 不能用 `machines.connected_at/disconnected_at` 或 `agents.status/activity` 判断当前可达性。`@all` / `@online` 是 best-effort broad wake policy：持久层只基于 scope、ownership、subscription 和 durable auto-wake intent 产生候选，实际 online/running/start-or-deliver 判断由 daemon hub 当前连接内存完成；fanout 失败或离线状态通过 realtime wake status / retry path 体现。

#### 4.6.3 连接生命周期与状态对账

daemon 侧连接特征：

- 自动重连
- 断线期间上行状态不保证完整
- 每次重连后都会发送 `ready(runningAgents=...)`

因此 server 侧需要：

1. 同一个 `machineApiKey` 同时只保留一条连接
2. WS 重连后用 `ready.runningAgents` 做状态对账：
   - machine online 只写入当前 hub 连接状态并 emit realtime；disconnect 同理 emit offline，不写 `machines.connected_at/disconnected_at`
   - `ready.runningAgents` 只作为 daemon 的暂时自报，写入当前连接内存，用于本次连接上的 `agent:deliver` vs `agent:start` routing
   - reconnect 时替换该连接内存中的 running set；不要清空或重建 TiDB 里的 live agent instance/state
   - fresh `agent:status` / `agent:activity` / `agent:session` / `agent:trajectory` 继续作为 realtime event 转发；缺失 fresh 上报时，UI 应显示 unknown/idle，而不是从 DB 恢复旧状态
   - 未来如需要跨 server 共享这些临时状态，使用 Redis/TTL 或等价 ephemeral store，不使用 TiDB

#### 4.6.4 `agent:deliver` 与 `agent:deliver:ack`

- `agent:deliver` payload 为 `{ agentId, seq, message }`
- `message` 使用 agent runtime 的 JSON-ready delivery message shape（代码中历史类型名仍为 `ReceiveMessage`，但不再对应 server `/receive` endpoint）
- `deliver` 是一次 daemon handoff / 唤醒消息；server retry 与诊断事实在 `scope_fanout_outbox` payload/status 中，daemon 接受后进入本地 runtime delivery / continuation inbox
- `agent:deliver:ack` 现在表示 daemon 侧是否接受了这次 handoff：
  - `accepted=true`：daemon 的 current accepting Process 已把消息交给 runtime transport，或持久化进该 Process 自己的 continuation queue
  - `accepted=false`：daemon 当前没有 accepting Process（或 current Process 无法接受该 handoff）；server 应 fallback 到 fresh `agent:start`
- `agent:deliver` 不解析 daemon-local slash command，不同步/选择 agent profile 与 runtime，也不创建、resume、replace 或 restart Process；短暂 ACK context 只约束 handoff，不拥有 Process lifetime
- 即使 `accepted=true`，也仍然不代表 agent 已处理消息；它只表示 daemon 已接受这次 handoff，后续 runtime 是否消费由 daemon-local continuation inbox / runtime 状态决定

#### 4.6.5 Workspace 浏览的 request/response 适配层

服务端内部为每条 machine WS 连接维护一个 `daemonClient`：

- `agent:workspace:read`：使用当前沿用消息形状里的 `requestId` 做并发复用
- `agent:workspace:list`、`machine:workspace:scan`、`machine:workspace:delete`：历史分析过的 `0.24.0` 消息形状里仍然没有 `requestId`
- `agent:reset-workspace`：历史分析过的 `0.24.0` 仍然只有下行命令，没有成功回包

因此首版采用最简单且兼容的适配策略：

- 读文件：`requestId -> waiter`
- 其它 workspace 操作：每台 machine 同时只允许一个 in-flight 请求，串行执行
- `reset-workspace`（当前实现：`POST /api/agents/:agentId/reset`）：
  - `clearWorkspace=false`：不要求 machine 在线；保留当前 auto-wake 状态，并 best-effort `stop`。对支持本地 resume 的 daemon，server 在 stop 命令上携带 `clearSession`，daemon 清理本机 `.daemon-state.json` session resume metadata；同时 server 可清空 legacy `agents.session_id` mirror，以保证未上报 `agent:local-session-resume` 的旧 daemon 也能 fresh start
  - `clearWorkspace=true`：要求 machine 在线；下发 `agent:reset-workspace` 后返回 `200`；同时保留当前 auto-wake 状态
  - 若 machine 离线且 `clearWorkspace=true`：返回 `503`（`Machine is offline`）
- 超时/取消（当前实现）：主要依赖 HTTP request context（客户端超时/取消即终止）；未额外设置统一 server-side timeout

权限建议：

- 工作区浏览与删除/重置仅 `owner` 可用
- 由于这是低 QPS 的 debug 能力，串行化这些操作比发明额外相关 ID 更简单也更稳

### 4.7 数据面协议：Internal Agent HTTP API

实现位置（As-Is）：

- HTTP 路由与 request/response shape：`internal/api/agent/routes.go` + `internal/api/agent/api_*.go`
- 鉴权与主语模型：`internal/api/agent/helpers.go`（handler auth helper）+ `internal/store/agent/service.go`（agent auth）+ `internal/store/agent_tokens.go`（附件下载 token 解析）
- 核心读写原语：`internal/store/message`（写、history、search/read-check、scope projection、routing/fanout payload）以及 `internal/store/message/{service.go,projections.go,reply_threads_store.go}`（channel/DM/reply-thread access、`resolve-channel` target routing、channel/DM/member projection / open-list / reply-thread summary）

**基础约定**

- 鉴权：`Authorization: Bearer <agentAuthToken>`
- 当前 AgentTeam internal agent contract 覆盖的内部端点：
  - `POST /api/v1/workspaces/:workspaceId/agents/:agentId/runtime/send`（返回 `messageId`；支持 `attachmentIds`）
  - `POST /api/v1/workspaces/:workspaceId/agents/:agentId/runtime/resolve-channel`（把 daemon-style target 解析为 canonical `scopeId`）
  - `POST /api/v1/workspaces/:workspaceId/agents/:agentId/runtime/upload`（multipart 上传图片附件）
  - `GET /api/v1/workspaces/:workspaceId/agents/:agentId/runtime/workspace`
  - `GET /api/v1/workspaces/:workspaceId/agents/:agentId/runtime/history?channel=<#slug|dm:@peer|#slug:shortid|dm:@peer:shortid>&limit=50&before?=&after?=`
  - `GET /api/v1/workspaces/:workspaceId/agents/:agentId/runtime/search?q=<query>&channel?=&limit?=&cursor?=`
  - `GET /api/v1/workspaces/:workspaceId/agents/:agentId/runtime/messages/:messageId/read-check`
  - `GET /api/v1/workspaces/:workspaceId/agents/:agentId/runtime/scopes/:scopeId/members`
  - `GET /api/v1/workspaces/:workspaceId/agents/:agentId/runtime/review-context`
  - `PUT /api/v1/workspaces/:workspaceId/agents/:agentId/runtime/review-context`
  - `DELETE /api/v1/workspaces/:workspaceId/agents/:agentId/runtime/review-context`
  - `GET /api/v1/workspaces/:workspaceId/agents/:agentId/runtime/threads/:threadChannelId/review-context`
  - `PUT /api/v1/workspaces/:workspaceId/agents/:agentId/runtime/threads/:threadChannelId/review-context`
  - `DELETE /api/v1/workspaces/:workspaceId/agents/:agentId/runtime/threads/:threadChannelId/review-context`
  - `GET /api/v1/workspaces/:workspaceId/agents/:agentId/runtime/tasks`
  - `POST /api/v1/workspaces/:workspaceId/agents/:agentId/runtime/tasks/get`
  - `POST /api/v1/workspaces/:workspaceId/agents/:agentId/runtime/tasks`
  - `POST /api/v1/workspaces/:workspaceId/agents/:agentId/runtime/tasks/update`
  - `POST /api/v1/workspaces/:workspaceId/agents/:agentId/runtime/tasks/set-claim`
  - `POST /api/v1/workspaces/:workspaceId/agents/:agentId/runtime/tasks/add-checkpoint`
  - Agent Task wire 由 `specs/daemon/mcp-tool-design-contract.md` 的 workspace-scoped runtime contract 约束。
- daemon `view_file` 工具会通过 app API 下载附件：
  - `GET /api/attachments/:attachmentId`（必须支持 agent auth token）

review context 的当前实现语义：

- agent-owned legacy review context 仍沿用 `agent_output_targets`，每个 agent 一个显式 slot，用于现有 agent review 页面；未配置时返回 `{ "context": null }`
- thread-owned review context 存在 `review_contexts`，第一版只开放 `subject_type=thread`，`subject_id` 使用 canonical reply thread `threadChannelId`，同一 thread 下每个 `source_agent_id` 最多一个 active context
- Agent Runtime 的 `PUT /review-context` 允许 agent 为自己写入 `kind/rootPath/displayName/headRef/baseRef/comparisonMode`
- Agent Runtime 的 `PUT /threads/:threadChannelId/review-context` 允许 agent 为可访问 reply thread 写入自己的 source-agent context：`kind/rootPath/displayName/headRef/baseRef/comparisonMode/prUrl`；`DELETE` 幂等清理该 agent 在该 thread 下的 context
- path 校验继续复用 daemon `agent:output-target:validate`
- git compare 默认值校验继续复用 daemon `agent:review:summary`
- human app 的 agent git review endpoint 在没有 legacy 显式 review context 时，会向 daemon 传空 `targetPath` 并回退到 session workdir；thread review endpoint 只读取 `review_contexts`，没有 thread context 时不会回退到 agent session workdir
- `output-target/files` 与 `output-target/read` 仍然只认 legacy 显式 context

#### 4.7.1 当前 bridge/client contract 约束：internal API 错误处理

基于当前已实现的 bridge/client 行为与历史外部 artifact 交集，可以确认：

- agent-facing queue-read MCP 已移除；daemon-local continuation inbox 只由 delivery state machine 消费，不再调用 server `/receive`
- `list_workspace`：仍会直接 `await res.json()` 且不检查 `res.ok`
- `send_message` / `read_history` / `upload_file`：虽然会检查 `res.ok`，但同样会先 `await res.json()` 并读取 `data.error`

因此 internal API 必须同时满足：

1. **语义正确**：鉴权失败返回 `401/403`，服务内部错误返回 `5xx`
2. **兼容 bridge**：即使非 2xx，也返回可解析 JSON（并尽量带上 bridge 期望的字段形状）

推荐错误体：

- Agent Runtime `POST /send`：`{ "error": "..." }`
- Agent Runtime `POST /resolve-channel`：`{ "error": "..." }`
- Agent Runtime `POST /upload`：`{ "error": "..." }`
- Agent Runtime `GET /history`：`{ "error": "..." }`
- Agent Runtime `GET /search` / read-check / scope members：`{ "error": "..." }`
- Agent Runtime `GET /workspace`：`{ "error": "...", "channels": [], "agents": [], "humans": [] }`

#### 4.7.2 Channel 标识解析规则（按公开 member reference 重定义语义）

internal API 的 `channel` 参数格式沿用当前已实现的 “target string” grammar；历史外部 daemon artifact 也使用过相近格式：

- Channel：`#<channel-slug>`
- DM：`dm:@<peer-human-member-id>` 或 `dm:@<peer-agent-name>`
- Reply thread：
  - Channel thread：`#<channel-slug>:<shortid>`
  - DM thread：`dm:@<peer-human-member-id>:<shortid>` 或 `dm:@<peer-agent-name>:<shortid>`

兼容性约定：

- `DM:@...`（旧格式）仍会被接受，但 canonical 形式为 `dm:@...`（lowercase）

解析规则：

- `#...` → `thread.type=channel`，`channel_slug = 去掉 #`；若含 `:<shortid>` 则表示 reply thread target
- `dm:@...` / `DM:@...` → `thread.type=dm`，`peer_member_reference = 去掉 dm:@`；若含 `:<shortid>` 则表示 reply thread target
- 其他格式一律 `400 {"error":"invalid channel identifier"}`

校验规则：

- `channel_slug` 按 channel slug 校验
- `peer_member_reference`：
  - human 必须匹配 `member.id`
  - agent 优先匹配公开 `agents.name`
  - legacy `agent member.id` 仍允许解析，但只作为兼容 alias，不是公开推荐写法
- `shortid` 允许 `6-8` 位 hex（daemon 通常使用 `messageId.slice(0, 8)`）；服务端必须检测歧义并返回 `409`
- human `member.id` 比较时大小写敏感，服务端生成后即固定；agent `name` 规范化为 lowercase，并按其公开值解析

#### 4.7.3 Internal API 语义细节（建议首版定死）

`POST /api/v1/workspaces/:workspaceId/agents/:agentId/runtime/send`

- 当前 `send_message` tool contract 使用 `{ target, content, attachment_ids? }`，并会转为 body `{ target, content, attachmentIds }`
- 约束：
  - `target` 必填
  - `content` 与 `attachmentIds` 至少其一非空（只发附件也允许）
- 语义：
  - `target = dm:@peer`（无 shortid）：server 以 `(sender=agent, peer=peer)` 查找 / 创建 DM thread
  - `target = #slug`：要求 agent 已加入该 channel；否则 `403`
  - `target = #slug:shortid` / `dm:@peer:shortid`：表示 reply thread target
    - 若 reply thread 已存在：校验可访问性后写入该 thread
    - 若 reply thread 不存在：服务端需根据 `shortid` 在父 thread 内解析出唯一的 `parent_message_id` 并自动创建 reply thread（否则 daemon 的 reply hint 会失效）
      - 当前实现不会把这个解析偷偷限制在 `scope_messages`：若热层未命中且父消息已归档，会显式扫描冷归档 chunk 解析 shortid，优先保证协议正确性，而不是额外引入永久消息索引表
      - `shortid` 找不到：`404`
      - `shortid` 歧义：`409 {"error":"thread identifier is ambiguous"}`
- 若请求依赖既有 DM thread（例如 Agent Runtime `GET /history?channel=dm:@peer`）但 DM 不存在：返回 `400 {"error":"dm does not exist; send a message to start a new DM"}`
- 创建消息后，服务端必须把**发送者**在该 scope 的 `last_read_seq` 推进到新消息 `seq`
- `attachmentIds`（可选）：
  - 必须是该 member 在同一 scope 内上传、且尚未被 claim（`message_id IS NULL`）的附件
  - server 在同一事务内将附件 `message_id` 绑定到本次新消息，并在下行消息中返回 `attachments: [{ id, filename }]`
  - 它定义本次消息 claim 的附件集合与后续下载鉴权边界；其数组顺序不是消息排版协议。正文排版由 `content` 中的 `attachment://<attachmentId>` Markdown 引用位置决定，发送方只能引用同一请求中被 claim 的 ID
  - 图片引用使用 `![alt](<attachment://...>)`；视频、音频和普通文件使用标准 `[label](<attachment://...>)`。支持 MIME-aware media 的客户端可把 video/audio link 升级为内嵌播放器，旧客户端保持下载链接；未被正文引用的附件仍作为 message sidecar 展示

`POST /api/v1/workspaces/:workspaceId/agents/:agentId/runtime/resolve-channel`

- body：`{ "target": "<#slug|dm:@peer|#slug:shortid|dm:@peer:shortid>" }`
- response：`{ "scopeId": "<scopeId>", "targetKey": "<scopeId>" }`
- 语义：
  - `#slug` → channel `scopes.id`
  - `dm:@peer` → DM `scopes.id`（若 DM 不存在则创建）
  - `*:shortid` → reply-thread `scopes.id`（若 reply thread 不存在则自动创建）

`POST /api/v1/workspaces/:workspaceId/agents/:agentId/runtime/upload`

- multipart form：
  - `scopeId`：必填，来自 `resolve-channel`
  - `file`：必填，且必须是单文件
- 推荐约束（当前 agent toolchain 共同约束）：单文件最大 `5MB`，仅允许 `jpeg/png/gif/webp`
- response：`{ "id": "<attachmentId>", "filename": "...", "sizeBytes": 123 }`

`GET /api/attachments/:attachmentId`

- daemon `view_file` 会使用 `Authorization: Bearer <agentAuthToken>` 下载附件，因此必须支持 agent auth token
- 仅允许下载已被 message claim 的附件（避免“未发送附件”被直接访问）
- 当前 endpoint 返回完整 blob，不实现字节范围响应；Web 内嵌视频因此只适用于受 `20 MiB` 上限保护的短视频。流式首帧与 seek 需要后续增加短期授权媒体 URL、`Accept-Ranges`/`Range` 和 `206 Partial Content`

`GET /api/v1/workspaces/:workspaceId/agents/:agentId/runtime/history`

- `after`：返回 `seq > after` 的消息
- `before`：返回 `seq < before` 的消息
- 若同时为空：返回最新一页
- 当前实现中，agent `history` 是纯读取上下文，不推进 `scope_memberships.last_read_seq`；human read/unread 仍由 human read actions 和 browser sync cursors 维护
- `last_read_seq` 返回调用时可见的当前读位置（兼容字段），不是本次 `history` 的消费确认
- `historyLimited` 首版固定为 `false`

#### 4.7.4 精确 JSON 字段（按 chat-bridge 期望，必须照抄）

> 字段名必须兼容当前 bridge 的实际反序列化代码；字段名风格混用也要照抄。

`POST /api/v1/workspaces/:workspaceId/agents/:agentId/runtime/send`

```json
{ "messageId": "00000001d34db33f..." }
```

`GET /api/v1/workspaces/:workspaceId/agents/:agentId/runtime/workspace`

```json
{
  "channels": [{ "name": "all", "description": "…", "joined": true }],
  "agents": [{ "name": "alpha-agent", "status": "active" }],
  "humans": [{ "name": "m8x2ra" }]
}
```

这里的 `name` 字段保持兼容，但语义已经变成：

- `humans[].name = human member.id`
- `agents[].name = agent public handle（`agents.name`）`

这是一条 internal protocol 兼容语义，不是默认 Web UI label 规则。任何 human-visible UI 若消费这类 payload，必须先经过 display projection：human 默认显示 `displayName`，agent 也默认显示 `displayName`；agent 的 `agents.name` 只作为稳定公开 handle / target。

`GET /api/v1/workspaces/:workspaceId/agents/:agentId/runtime/history`

```json
{
  "error": "",
  "messages": [
    {
      "seq": 1,
      "id": "00000001d34db33f...",
      "senderType": "human",
      "senderName": "m8x2ra",
      "authorProfile": {
        "actorType": "human",
        "memberId": "m8x2ra",
        "targetId": "m8x2ra",
        "displayName": "Alice"
      },
      "content": "hi",
      "createdAt": "2026-03-09T12:34:56.789Z",
      "attachments": [{ "id": "att_...", "filename": "pixel.png" }]
    }
  ],
  "has_more": false,
  "historyLimited": false,
  "historyLimitMessage": "",
  "last_read_seq": 1
}
```

这里的 `authorProfile` 是 agent history/read 语义的 canonical author
projection。`senderType` / `senderName` 只属于旧 daemon/desktop
internal protocol 兼容字段，由 API DTO adapter 从 canonical `AuthorProfile`
派生；store runtime/read model 主路径不得继续携带或消费 sender-shaped author
字段。浏览器/API/socket/app-callback wire 只使用 `authorProfile` / typed
`source`，不再提供 `senderDisplayName`、`senderServiceKind` 或
`senderServiceSlug` 之类的 sender-shaped display projection。

#### 4.7.5 Task Board API 与执行面

实现位置（As-Is）：

- Task board 领域实现：`internal/store/taskboard/*`
- browser API：`internal/api/task/routes.go` + `internal/api/task/api_*.go`
- agent runtime API：`internal/api/agent/routes.go` + `internal/api/agent/api_tasks_agent.go`
- live events：`internal/store/taskboard/events.go` → `internal/api/im/task_change_bridge.go` → `internal/api/im/live_emitter.go`

权限与范围：

- Task 归属 channel，workspace hub 只聚合当前成员可见 channel 的 Task。
- human read/write 由 `agentteam-core/access` 的 `channel.read` / `channel.post` 决策约束；agent runtime 使用已认证 agent principal，并由服务端确定 actor。
- lifecycle 固定为 `todo`、`in_progress`、`in_review`、`done`。Task 行只保存一个可空的 Human / Agent `assignee`；claim 是 self-assignment。Kind、Priority、blocker、checklist、subtask 与独立 Review Request / decision 是其他协作事实。

Browser API：

- collection：`GET|POST /api/v1/workspaces/:workspaceId/channels/:channelId/tasks`
- workspace/channel browse：`POST /api/v1/workspaces/:workspaceId/tasks/{workspace|channel/:channelId}/{browse|board/browse}`
- workspace filter candidate discovery：`GET /api/v1/workspaces/:workspaceId/tasks/filter-options`；默认一次聚合返回 channel、assignee、kind、priority，按需搜索/分页通过同一 URL 的 query 参数完成
- item：`GET /api/v1/workspaces/:workspaceId/channels/:channelId/tasks/:taskNumber`、`GET|PATCH|DELETE /api/v1/workspaces/:workspaceId/tasks/:taskId`
- lifecycle/detail：claim、unclaim、block、unblock、subtasks、checkpoints、timeline、read state、execution history、announcements、notification policy、active targets 与 collaborators
- workspace taxonomy/saved views：`/task-board/options`、`/task-board/saved-views` 与 channel view state
- Task discussion 通过共享 message API 写入 Task scope。

Agent toolchain：

- 七个 model-visible tools 为 `task_list`、`task_get`、`task_create`、`task_update`、`task_set_claim`、`task_checkpoint`、`task_delete`。
- transport 路径位于 `/api/v1/workspaces/:workspaceId/agents/:agentId/runtime/tasks*`；tool input 使用闭合字段集合，agent principal 与 workspace binding 由 runtime 注入。
- `task_list` 负责浏览、筛选和查询；`task_get` 返回执行所需的 detail；`task_update` 覆盖状态和 Task properties；`task_set_claim` 以幂等 desired state 批量 claim/release。

Web 与数据流：

- 当前 Web routes 为 `/tasks`、`/tasks/views/:viewId`、`/tasks/channels/:channelId`、`/tasks/settings` 与 `/channels/:channelId/tasks/:taskNumber`。
- workspace `Tasks` rail entry 直接进入 Task hub，不挂载 Task 二级 sidebar，也不经过整页 lazy fallback 或 `/tasks` 到默认 view 的二次重定向。Task Header、View、search、结构化 filters、sort 与 display 组成稳定 shell，下面的 Board/List browse 数据按 query 与 pagination 渐进加载；`Board / List` switch 位于页面 title header action。Channel/embedded Task surface 不提供 workspace View，`Board / List` switch 紧跟 search。
- View 复用 `/task-board/saved-views` CRUD。选中 preset 后，search、quick/structured filters、sort、done collapse 与 presentation mode 由 `TaskBoardViewState` 驱动；system preset 不可更新，偏离 system preset 的临时状态显示为 Custom，saved preset 可新建、更新、恢复和删除。Card field visibility 从 view state 拆出，由 `useTaskBoardDisplayPreference` 按 browser + workspace + user 保存在 localStorage，workspace hub 与所有 Channel Task surface 共享且不写服务端。Task browse client 校验 response `queryEcho` 与请求中的 search/sort/filter 语义一致，避免前后端版本不匹配时静默展示未过滤结果。
- workspace hub 与 channel list 使用 row cursor pagination；board 使用 column-local cursor pagination；search/filter/sort/count 先作用于完整可见结果集。
- workspace hub 与 Channel Task surface 的动态 filter candidates 由 `internal/store/taskboard/filter_options.go` 在 browse 相同的 scope-membership ACL 上独立查询，API owner 是 `internal/api/task`。Channel 通过可选 `channelId` 限定候选集，但不绕过 workspace membership join。Web 的 `useTaskBoardFilterOptions` 统一聚合查询和 `dimension=assignee&q=...` 显式搜索；旧 server 返回 404、请求失败或单个维度为空时，已缓存 browse page 只补齐可用候选，不替代完整 discovery 集合。
- Browser Task detail HTTP wire 仍返回 checklist、context refs、artifact、checkpoint 等完整 task detail facts；当前 React `TaskDetailPage` 只展示标题、描述、status / priority / assignee / kind、子任务、agent context、通知控制与 task-native Activity / discussion。Review Request facts 仍由 task store/service 持有，但 human review routes 已退役，当前 Browser UI 不暴露 request/decision 操作。Agent `task_get` / `task_update` / `task_checkpoint` 暴露其明确 Task wire fields，不包含 Review Request mutation。
- Task mutation 写入 Task facts 和 typed activity；human/agent authored discussion 写入 Task scope message timeline。
- create 只有一个 operation，可选 `assigneeRef`；assign / claim / reassign / unassign 都写 typed Activity。Task attention planner 以 `Watchers ∪ assignee ∪ current Message direct mentions − actor` 固化 recipient snapshot，并通过 `scope_fanout_outbox` 驱动 Human Inbox unread 与 Agent delivery / wake。
- committed Task changes 经 `runtime.task_created`、`runtime.task_updated`、`runtime.task_deleted` live events 触发客户端 cache refresh。

Agent Task wire 仅由 `specs/daemon/mcp-tool-design-contract.md` 的 workspace-scoped runtime contract 记录。

错误体约定：

- 所有非 2xx 都返回 `{ "error": "..." }`
- `claim` 里的冲突优先走 `200 + results[]`，只把整批无法解析/鉴权失败当成 4xx/5xx

### 4.8 核心运行路径

实现位置（As-Is）：

- Public 写入口：`internal/api/im/messages_handler.go`（最终写入由 store 原语负责）
- Internal 写入口：`internal/api/agent/api_messages.go`（最终写入由 store 原语负责）
- 消息写入原语（含 `32KiB` 上限校验）：`internal/store/app/message_append.go` 与 `internal/store/message/scope_store.go`
- Agent continuation delivery：server 不再提供 `/receive` pending-message 拉取；daemon 通过 `agent:start` / `agent:deliver` 接收 wake payload，并从本地 continuation inbox 自动递交尚未 surface 的消息
- History 冷热读：`internal/store/message/history.go` + `internal/archive/*`
- 消息冷归档 worker 当前未运行

#### 4.8.1 消息写入路径（最重要）

当 human 或 agent 发送一条新消息：

1. 开启 core main-path 事务
2. 在事务内分配序列：
   - 通过 portable helper 先 `UPDATE workspaces SET latest_message_seq = latest_message_seq + 1 WHERE id = ...`，再在同一事务里读取 `latest_message_seq` 作为 `global_seq`
   - 通过 portable helper 先 `UPDATE scopes SET latest_seq = latest_seq + 1, latest_global_seq = global_seq, latest_message_at = now WHERE id = ...`，再在同一事务里读取 `latest_seq` 作为 `seq`
3. 插入 `scope_messages(id, workspace_id, scope_id, seq, workspace_global_seq, author_member_id, author_type, message_type, content, metadata, created_at, updated_at)`
4. 写入 message fanout intent：
   - 普通 channel / DM / reply-thread 消息不写 per-recipient dense delivery rows；human unread 由 `scope_memberships` cursor 与 current `scope_notification_settings` 偏好推导；agent wake/delivery execution truth 由当前事件计算后写入 `scope_fanout_outbox` payload/status
   - parent-scope direct public-agent mention resolves agents already present in `scope_memberships`; it changes the current fanout reason / priority on top of normal parent-scope dense/default delivery, so ordinary `@human` or public `@agent` does not suppress other public auto-wake agents' channel delivery
   - private-agent `@mention` resolves owner-private entries for the parent channel/thread/task scope and requires active scope membership plus private authority; it changes fanout reason / priority and does not create a second transcript row, parent mirror, or activity row
   - direct agent mention、owner-private context/source grant、human direct mention 不写 `scope_activity_events`；Task assignment changes are ordinary typed Activities whose attention is delivered by a non-message scope fanout intent, without hidden message or assign/start mode
   - reaction signal、Task typed Activity projection 等没有 message row 的 activity 写 `scope_activity_events`；Task feature code 只能声明 typed event intent，并必须通过 task timeline writer 写入；pure Task Activity 可以产生 targeted attention，但 paired `discussion_added` 不得与 owning Message double fanout
   - human direct personal mention 仍可穿透 quiet/mute，但作为 message fanout reason，而不是改写默认 subscription state
   - owner-private context view 由当前 scope 中符合 owner-private 可见性规则的 parent rows、collaborator grants、message readability predicate 与 owner_quote source refs 组成；non-owner 普通 message 不会自动唤醒 private agent，除非已通过 owner-private filter、collaborator grant 或 explicit Share/Forward owner_quote 授权
   - private-agent authored reply 使用 parent channel/thread/task target，并直接写入 parent timeline；输出不再经过 private transcript、manual publish 或 auto-publish materialization
   - thread-level private execution context 按 parent thread/source scope 与 hidden `scope_execution` 绑定；新增 channel private entry 不会 retroactively 改写历史 thread delivery/cursor，只在后续显式唤醒或授权事实中生效
5. 不因 authoring 自动推进发送者自己的 read cursor；`scope_memberships` read cursor 只在明确 read / open / consume 时推进，self-authored row 通过 author suppression 从 unread surfaces 中排除
6. 提交事务
7. 事务提交后再做副作用：
   - 通过 `scope_fanout_outbox` lease / retry 边界执行 daemon delivery 与 realtime fanout
   - 按 `auto_wake_enabled` + 当前 machine 运行事实通知 agent 所在 machine（`agent:deliver` 或 `agent:start`）
   - 向 Web Socket.IO 广播事件（`internal/appws`）

这个流程有几个重要性质：

- 不需要分布式锁，只依赖 `workspaces` / `scopes` 单行上的普通事务锁
- 新消息一旦提交，就已经在热层可读
- 归档永远是异步后台行为，不阻塞写入
- Web ShareFlow 面向 channel、DM 或 subscribed thread 等 ordinary targets，并通过目标 timeline 中的 concrete `owner_quote` message + source refs 保留 source attribution。

Task execution 使用 Task detail、Task discussion、Task references、announcements 与 ordinary ShareFlow；owner-private operations 由 authenticated runtime session、explicit task target 和 owning API authorization 提供。

#### 4.8.1A Task-native execution（As-Is）

Owner-private execution is represented as a private projection/context substrate behind parent targets. The default product path is Task-native:

- `scope_activity_events` carries reaction signals and Task typed activity projections. Task mutations declare `TypedActivityIntent` entries; the task timeline writer owns event ID allocation, activity sequence, actor participant setup, and projection insertion. Human/agent-authored content uses the message path. Task timeline read DTOs expose actor and provenance through the shared `authorProfile` / `source` substrate.
- Ordinary channel / DM / reply-thread delivery uses scope membership, notification state, policy windows, and read cursors.
- Task-facing APIs cover execution history, announcements, Notifications & output policy, active target set/clear, and collaborator ingress/audit. Web entries use Task detail, Task discussion, Task references, `owner_quote` source refs, and Task announcements.
- Owner-private execution is carried by authenticated server/runtime ownership while agent-visible targets remain explicit parent channel/thread/task targets.
- Task-native operations keep source attribution through structured `owner_quote` messages, source refs, collaborator grant/readability predicates, and outbox facts.

The current schema contains the Task-native scope delivery model and its runtime tables.

#### 4.8.2 `read_history` 路径（按冷热分层读取）

channel/thread/DM `read_history` 读取逻辑：

1. 先查 `scopes`，拿到：
   - `latest_seq`
   - `archived_until_seq`
2. 如果请求范围完全落在热区间：
   - 只查 `scope_messages`
3. 如果请求范围完全落在冷区间：
   - 根据 `before/after` 对应的 `seq` 范围，直接计算需要读取哪些归档对象
   - 直接从归档存储读取（filesystem：本地文件；S3：GetObject）；若未来实测确有必要，再补本地缓存优化
4. 如果请求范围跨冷热边界：
   - 分别从 `scope_messages` 与 cold object reader 取数据
   - 在 server 内存里 merge 后裁剪成一页
5. 最终统一按 `seq ASC` 返回给调用方

Owner-private context `read_history` 读取逻辑：

- agent-visible `channel` 参数始终是 parent channel / DM / reply-thread target；hidden owner-private context 由 authenticated runtime session / current delivery binding 提供；
- private agent 不因 parent target 获得完整 parent transcript，只能读取 owner-private filtered parent rows、collaborator-granted rows 和 owner_quote source refs；
- reply-thread 读取会校验 thread root parent message 对当前 owner-private context 可见；
- agent `read_history` 不推进 read cursor；human read/unread cursor 仍在 parent scope membership 上维护，owner-private context 只参与 visibility 过滤，不维护单独 local transcript seq。

默认最新页为什么会很快：

- 它几乎总是完全命中 `scope_messages`
- 并且查询只需要 `scope_messages_scope_seq_key(scope_id, seq)` 的倒序扫描 + limit

owner-private v1 的 `read_history` 边界（当前实现）：

- owner-agent DM 仍是 owner-only，全量 DM 历史可读
- channel / thread 中，agent 只会读到：
  - owner human 发出的 shared message
  - 同一 owner 旗下 agent 发出的 shared message（含自己）
- 非 owner human 的 shared backlog 仍然不可见
- `@mention` / direct delivery / thread inheritance 只决定即时 wake，不决定 `read_history()` 的 shared backlog 可见性

#### 4.8.2A Knowledge Q&A 托管执行（As-Is）

- 每个 Knowledge Base 的 `allow_knowledge_qa_without_source_access` 默认关闭且只有 KB manager 可修改。开启后，当前 workspace member 仍须具有非 `none` 的 KB product role，但 MeshX hosted-Q&A catalog 可在没有 materialized source-repository grant 时签发该 KB 的 server-owned immutable roots；显式 `none` KB grant 继续优先拒绝。该策略只影响 Knowledge Q&A scope selector、root preparation 与 citation recheck，不创建、修复、撤销或扩张 AGS collaborator、credential、token，也不放宽 repository tree/blob/clone/download/issue/PR、generic KB retrieval 或直接 source API 的既有授权。
- `internal/api/knowledgeqa` 暴露 workspace-scoped availability、scope preview、session、turn、retry、stop、history 和 citation API。availability 读取当前组织开关与 agentteam-maas 组织级模型可用性，并向普通成员返回完整托管模型列表以及每个模型经 MaaS Chat Completions compatibility profile 保真映射后的推理级别；只有同时具备 `chat_completions`、`chat_completions.reasoning_effort`、`chat_completions.tools`、profile-driven request 会实际发送的每个独立 MaaS feature，以及完整、精确、当前服务端支持的 profile 的模型才可执行，缺失/未知/冲突/歧义均 fail closed。OpenAI 当前额外要求 `chat_completions.tool_choice` / `chat_completions.parallel_tool_calls` 并暴露 `low` / `medium` / `high`；DeepSeek V4 thinking 省略这两个参数，因此不要求对应 feature，且只暴露不会误导语义的 `high`。create/retry 在服务端再次校验 model/reason 组合，浏览器不是授权边界。citation API 只通过 session/current-attempt owner 边界解析不透明 `kqc_*` 引用，在 MeshX 重查当前权限并确认历史 revision 后，由 AgentTeam 按 primary / insights / lexicon 生成产品内 `targetRoute`；浏览器不接收 repository citation metadata。
- `internal/api/knowledgeqa` 同时拥有需登录的回答分享边界：owner-only completed-turn create 会轮换 256-bit 邀请 token，global authenticated open/list/detail 将成功兑换绑定到 recipient user；不存在 anonymous/public reader。`internal/store/knowledgeqa` 只持久化 token SHA-256、去除所有 owner-scoped `agentteam-kqa-citation://` destination 的 question/answer snapshot、recipient open projection，以及不含 evidence body/credential 的 share-owned E-number→immutable citation metadata 映射；reader DTO 不投影 source workspace/session/turn、KB/Source/repository/owner citation/runtime metadata。点击分享回答中的 E-number 时，exact recipient/share/evidence endpoint 以接收者身份让 MeshX 重查当前来源权限：有 direct source authority 则右侧栏展示历史 revision 的有界代码并返回来源仓库产品路由；否则 redeemed share 只允许复用既有受限证据侧栏读取同一有界片段，不授予 repository navigation。Web 用 `/kb/shared#<token>` 避免 bearer 进入首次 HTTP target，登录/选择 workspace 后立即兑换并 replace 为稳定 `kqh_*` detail route；回答直达且加入左栏本地化的「来自分享」分区，该只读分区不依赖 recipient 当前组织的 Knowledge Q&A submit flag。
- `internal/api/org` 的 Knowledge Q&A 管理面只有组织启停开关与 content-free 使用概览；可用额度按面向用户的 AgentTeam 积分展示而非原始 credit atoms；不存在平台 Knowledge Q&A 策略、继承模式或组织模型默认值。
- `internal/store/knowledgeqa` 持久化组织开关、审计、session、turn、attempt、authorized roots、citation、activity，以及每个 session 可选的 derived runtime-context summary/generation/compacted-through boundary。canonical turn 与 formal assistant message 始终是对话事实；未压缩 tail 不重复持久化，而是通过 owner/session/current-turn bound 的常数次查询从全部 prior terminal turn 重建。缺失的组织开关记录等价于关闭，读取不 materialize；session 保存用户在入口选择的模型和推理级别，后续 turn 复制并沿用。当前 attempt 的 content-free groundedness warning 以 diagnostic activity 持久化，带 warning 的 completed turn 可显式 retry；server 只允许 retry session 最新 terminal turn。显式启用 `MESHX_KNOWLEDGE_QA_DEBUG_MODEL_OUTPUT` 时，worker 将失败 attempt 在既有 8 MiB MaaS response-read boundary 内的完整最后一次原始响应转为合法 UTF-8，并只在 turn 的 sparse `debug_model_output` 中保存一份；普通 `error_detail` 继续保持 2048-byte 上限。owner-only history 只为最新 failed turn 精确读取该字段并投影为 `debugModelOutput`，不把大字段带入普通 turn/history/runtime-context 查询。该开关默认关闭，成功响应、assistant message、activity、event、accounting checkpoint 与组织 metrics 均不携带该内容。worker 在渲染回答前分配并去重稳定 `kqc_*`，兼容性地在存在前一条语义 claim 时合并 citation-only claim，先删除模型提供的 citation destination，再仅把 Markdown 普通文本中的已验证 `[E*]` marker 链接到服务端分配的 `agentteam-kqa-citation://`，并移除相邻重复的同一已验证引用；list/table/code/Mermaid 结构保持不变，code span/fence 中的同形文本保持 literal。citation、canonical assistant message、runtime-context generation CAS 与 completed terminal transition 在同一事务提交。
- `internal/service/knowledgeqa.Worker` 从 TiDB durable queue 公平 claim attempt，执行前重查组织开关与 session selection，并在每个 model-backed turn 无条件装载同 session runtime context；completed 投影 question/final answer，no-answer/failed/canceled 只投影 question/status，不发送 partial output、error detail、tool trace 或历史 evidence。summary 与 completed answer tail 在进入 model-visible context 前剥离服务端 `agentteam-kqa-citation://` destination 并保留可见 marker，避免展示层的 durable route 被后续模型复制。之后经 `forwarder.KnowledgeQARuntimeGateway` 完成 MeshX scope authorization、托管模型调用、aggregate usage 与逐次 settlement lineage 持久化，以及 canonical assistant message terminal commit。
- Knowledge Q&A turn 页面首次进入时读取一次 bounded owner projection；create / retry / stop、worker activity / status 和 terminal write 之后，通过现有 workspace `events/stream` 持久化并投递 owner-local `knowledge_qa.turn.updated`，Web 精确 invalidate 对应 thread 的 turn query；与 formal assistant message 同事务提交的 terminal event 额外标记并刷新该 thread 的 message query，assistant message 同时走普通 post-commit message fanout。WebSocket 每次重连刷新当前 workspace 中 active turn queries；由于 turn write 与 IM stream event 仍是两个事务，最新 turn 非终态或尚未出现时保留 15 秒 safety reconciliation，终态立即停止，不再使用 500ms activity polling。`generating` 期间 worker 至多每 10 秒读取一次 exact owner-bound accounting checkpoint，仅在 model generation 跨过 10 个指数里程碑之一时追加 content-free progress activity；该读取不延长 checkpoint TTL，进度行增长与知识库规模无关且每 attempt 最多 10 条。
- Knowledge Q&A 回答正文与普通消息保持同宽并使用独立的 15px/1.7 排版；超过普通长消息阈值时默认完整展开，用户显式折叠后则保留该状态。证据引用在 topic 与认证后的分享回答 reader 中统一为中性的紧凑数字标记，用边框、底色与 hover/focus 状态表达可交互性，不仅依赖链接蓝色；Mermaid 围栏复用 conversation renderer 的内置 `neo` look，并按当前深浅外观选择 `neo`/`neo-dark` 主题，放在轻边框的可滚动图表容器中。topic 中点击证据时，desktop 复用 conversation 的可调整宽度右侧栏并让正文同步收窄，不再用浮层覆盖正文，mobile 保持 sheet；topic header 只保留最新 completed answer 的认证分享与订阅切换。activity 运行中展开，成功完成后折叠为含正数 search/read/source 计数的一行摘要，零值隐藏，失败/no-answer/canceled/warning 保持展开。
- `internal/service/knowledgeqa/runtime` 在每个 attempt 开始时用一次 authority-scoped batch query 校验全部 opaque root handles，并把它们绑定到授权时的 immutable commit；后续 tool call 只读 execution session，不按 root/action 回查 DB。根授权后、首个模型调用前，MeshX 用同一 runtime capability 从 MaaS `/models` 解析一次完整 profile，要求对象与本地支持的版本逐字段一致，并将副本冻结用于 exploration、compaction 和 context-overflow retry；每次调用都携带 profile ID 供 MaaS 与 route-bound profile 比对。OpenAI profile 使用 `tool_choice:"required"` / `parallel_tool_calls:false` / `store:false` / `n:1` / `max_completion_tokens`，DeepSeek V4 thinking 省略这些不接受的参数、依赖 `auto`、使用 `max_tokens`，并要求每轮 assistant tool-call 的字符串 `content` 与原样 `reasoning_content` 在 tool result 前完整回放。托管模型通过 non-stream Chat Completions 原生函数调用，以 `knowledge_search` / `knowledge_list` / `knowledge_read` / `knowledge_ref` 迭代探索，并以 `knowledge_answer` 的非空有序 Markdown claim 字符串数组或 `knowledge_no_answer` 的空对象结束；`knowledge_ref` 只在当前问题显式命名分支或 Git tag 时，把同一已授权 primary Source 的显式 branch/tag ref（annotated tag 会 peel 到 commit）解析为 attempt-local immutable root，不接受 revision expression、remote ref、跨仓库或跨 Source 选择，也不修改 Source。当前问题同时显式要求切换 mirrored Source 时，`knowledge_answer.sourceBranchChange` 才可引用已签发的 branch 类型 ref root 生成非变更型提案；tag root 不可用于变更 Source 默认分支；AgentTeam formal message 只保存 KB/Source ID、expected current ref 和 target ref，Web 要求独立确认，已有 mirror update endpoint 在 Source lock 下重验 editor 权限与 expected-ref optimistic fence，再通过已有 manual sync/index 流程应用。回答 prompt 要求先直接结论、简单问题不强制标题、复杂问题限 2–4 个语义段/精简列表、代码标识用反引号、引用紧随断言且默认保持可扫描长度；这些是写作 rubric，不增加 claim shape 上限。两个终止工具都必须独占响应，不接受旧 `knowledge_finish`、scalar claim、claim object 或模型提供的 evidence ID 数组；citation-only claim 在 terminal 前以 `answer_claim_citation_only` 进入同一 bounded structured repair。每个 response 接受最多 24 个 call ID 唯一的 function-call batch，全部 call 按响应顺序串行执行并依次回放匹配的 tool result，每个 attempt 最多执行 512 个 function call（包括两个终止工具），由于每个有效 response 至少包含一个 call，同一预算也限制有效 response 总数且 context-overflow recompaction 不重置预算；完整 batch 在 repository I/O 前按最坏结果大小预留 context，预留不足但有界错误结果仍可容纳时不执行 I/O，而是为该响应的每个 call 返回 `batch_too_large` 并让模型下一轮缩小 batch；summary 仍只接受一个 `knowledge_context_summary` call。普通文本、超限或重复 call、混合终止工具、DeepSeek 缺失 replay 字段，以及未知或跨 root 的 attempt-local 引用，都在整批 repository I/O 前拒绝且不进入 history；这些模型响应在同一 attempt 内共享最多五次 frozen-model、new-child-idempotency-key、unchanged-valid-history 修复调用；每次修复的 system contract 附带服务端白名单化的有界 JSON 校验反馈，只包含固定 error code、允许的 tool/field 名和静态 requirement，不回显模型参数值、parser 原文、handle/path/ref，无法安全解析 call envelope 的上游响应只使用通用 `response_contract_invalid`。usage/settlement 仍计入 attempt 但拒绝响应不消耗 action，第六次拒绝以最终稳定 content-free category 失败；显式 debug 开启时，失败结果同时附带各次拒绝的有序脱敏校验反馈与既有 8 MiB response-read boundary 内完整的最后一次原始 MaaS response，AgentTeam 将其写入专用 sparse 字段，不改变执行/history；不兼容旧 text-JSON action。每次 attempt 将 durable handles 映射成 `r*` root aliases，search/list 再签发只在本次探索有效的 `f*` / `d*` refs；`knowledge_list` 可以同时携带已签发的 `directoryRef` 与其所属 root 的冗余 `rootAlias`，执行前会将匹配组合规范化为同一目录动作，未知或跨 root 的组合在共享修复机会耗尽后仍 fail closed；只有显式 read 产生可引用 `E*` evidence。runtime context 没有固定 turn/16 KiB 截断；provider 明确返回 `upstream.context_window.exceeded`（或 prior context 触发独立 2 MiB process safety boundary）时，MeshX 用相同 frozen model、固定逻辑级 `low` reasoning（由 profile 映射到合法 upstream 值）压缩最旧 tail，保留新 tail，换新的 child idempotency key 重试。summary 使用单独的 `knowledge_context_summary` function schema，不暴露 exploration functions；summary 仅解析对话指代、从不作为 evidence，summary/retry model call 的 usage 与 settlement refs 合并进当前 attempt checkpoint，AgentTeam 只持久化成功返回的 summary candidate。底层 AGS narrow primitive 只对本地对象库执行 `git grep`、`git ls-tree`、`git show` 与 local branch/tag resolve，不创建 worktree、不暴露 repository path、shell、credential 或 network；model-visible root/tool result 也不包含 revision 或 citation metadata，只增加已授权 Source 的 display name/default ref，range read 保留包括空行在内的精确行边界。runtime 从 `knowledge_answer` claims 的精确 `[E<正整数>]` markers 派生 evidence IDs，并保留 ordered claim、evidence ID 与 citation metadata 的结构化关联但不生成 AgentTeam URL。最终 claims 通过 conversation Markdown renderer 渲染 list/table/code/Mermaid；claim 可以不带 evidence marker且不会因此生成 warning，引用本次不可用 evidence 时仍输出回答但生成 content-free warning。只有已验证 evidence 生成 citation；citation 持久化 bounded ref 名称/类型供 UI 显示 Branch/Tag，但 citation open 仍重查当前访问权并只按历史 immutable revision 读取代码。
- MeshX 在每次 hosted-model call 后、下一次 tool/model action 前，将 generation、aggregate usage 与 ordered settlement refs 写入 requester-bound content-free TTL checkpoint，并在普通退出时用短时 detached accounting context 标 terminal。generation 覆盖 exploration、失败/修复、context-overflow retry 与 context-summary call，不设独立数量上限。MaaS adapter 与 generic inference 边界在任何 accounting 离开 producer 前执行同一 closed contract：usage 只保留固定 token-counter 名称与不超过 `2^53-1` 的非负 integer-valued JSON number（MaaS wire 必须使用 integer JSON token），numeric type/range 在 float conversion 前校验，未知 provider 字段不进入 KQA aggregate（MaaS usage event 仍是 canonical 计费事实）；aggregate merge 再做 safe-integer addition 与 encoded-size 检查，overflow 时保持既有 exact aggregate、checkpoint 当前 provider ref 并返回 `knowledge_qa.maas_accounting_recovery`；已知字段的非法值同样返回 accounting recovery 并保留其余合法 projection；provider settlement ref 超过 255 bytes 时不进入 accounting lineage 或普通非-debug storage，而写入 `kqa_request_sha256_<64 lowercase hex>` synthetic recovery ref，其 digest 可由 attempt ID + checkpoint generation 重建 child idempotency key 后复算，同样终止该 model step；显式 debug 仍可只在既有完整 raw-response sparse 字段中保留原值。每个 physical call 前先为一个 ref 与最多 1,533-byte JSON escaping 预留容量；ordered lineage 最多 4,096 个 ref 且 encoded JSON 最多 4 MiB，aggregate usage JSON 最多 65,535 bytes。MeshX checkpoint、AgentTeam ordinary completion 与 accounting recovery 三条写路径都重复验证该 envelope；4 MiB lineage、usage document 与每项额外 256-byte native-JSON/row 余量的合计严格低于 TiDB 默认 6 MiB transaction-entry limit，不依赖 `LONGTEXT` nominal length 或非默认 TiDB 配置。容量耗尽在调用上游前返回 stable resource exhaustion；producer 非法 accounting 则先把 bounded projection/recovery ref checkpoint，再返回 accounting recovery，不会 strand 新 usage event 或继续 transient retry。模型调用后的 checkpoint 写入失败保留已返回的合法 usage/settlement lineage，并使用 `knowledge_qa.maas_accounting_recovery`，不得退化为 inference failure；每次 checkpoint 写失败都只携带 content-free 的 turn/attempt/generation/state 标识记录底层持久化原因。AgentTeam 遇到 execute transport/cancel ambiguity，或已返回 settled aggregate 的 terminal write 失败时，先 terminalize turn并把 exact attempt 标为 accounting recovery pending；独立 bounded worker 经 owner-bound internal recovery endpoint 合并 accounting，不重放模型、不重开 terminal turn。server crash 留下的 running checkpoint 只在 execution deadline 与 quiescence grace 后接受；pending scan 走 `(status,next_at,id)` 索引与 bounded backoff/expiry。
- MaaS 负责组织 entitlement、credit balance、模型 inventory/runtime capability 与最终 settlement；AgentTeam 不维护第二份模型 allowlist，也不在模型失效时静默切换。

#### 4.8.3 消息冷归档状态

消息冷归档 worker 当前未运行，热消息不会被后台任务移出 TiDB。`internal/archive` 继续拥有历史对象格式、filesystem/S3 driver 与已有冷对象的兼容读取。

重新启用写侧归档前，必须先满足 4.5 定义的附件 manifest/blob lifecycle、连续前缀、固定块边界、历史窗口和跨存储幂等要求；不得复用一个返回成功但不执行归档的占位 worker。

#### 4.8.4 daemon-driven continuation delivery 的实现策略

当前 agent-facing contract 不暴露 `wait_for_message()` / `receive_message()` 或本地 inbox pull tool。

- agent 工作进行中不主动轮询消息；daemon/runtime 在安全 input boundary 注入精确消息正文
- agent 没有剩余工作时直接停止，不再自己阻塞等待或轮询
- 对支持 idle stdin / native prompt API 的 runtime，daemon 在进程仍存活且处于 idle 时，可直接投递下一条消息正文
- 对声明 ack-aware active-turn delivery 的 runtime，daemon 可把消息正文交给 runtime 的 pending 输入队列，但只在 runtime 回放 correlated surfaced ack 后才把该消息视为已进入上下文；transport accept 只会设置 held freshness hint
- Codex app-server 的输入 framing 用 `ReadSlice` fragment 累积一条完整 raw line，并对包括换行在内的累计值执行硬 `32 MiB` 上限；translated output 按 launch generation 隔离，以最终 JSON wire encoding 的准确 bytes 做 bounded lossless queue/backpressure。Replacement/fatal 在等待 generation fence 前 abort 旧 queue，oversize、encode、short write 或非预期 writer failure 都是 fatal，不能静默 drop transcript/terminal event
- Codex TUI 运行态 `InputQueueState.pending_steers: VecDeque` 保存已提交 core、尚未被 committed history echo 的 steer；daemon 以 durable FIFO ownership rows 记录每条 delivery 的 `MessageID`、canonical `TargetKey`、`TargetSeq`、delivery class 与终态。普通 visible-reply obligations 按 target 聚合而非按消息维护 cycle flags；服务端 `sent` / `already_sent` 返回的 `TargetKey` + `SeenUpToSeq` 才能批量结算同 target 下已 surfaced 且不超过 frontier 的 rows。held、失败、本地 AcceptedSeq、旧 frontier 和其他 target 均不结算；更高 seq 会在后续边界重新 final-check，signal-only rows 不参加普通 final-check。512 cap 在 runtime mutation 前按 durable live owners 计数；已接受的 input 绝不能在 echo 时因 cap 丢失。
- daemon 对 accepted concrete payload 维护 bounded durable ownership journal：`queued` 可安全 replay，`inflight(dispatching|outcome_unknown)` 禁止 blind replay，`terminal(surfaced|failed)` 保留 bounded tombstone。`surfaced` 优先于 `failed`，允许迟到 authoritative evidence 升级 failed→surfaced，但 terminal 不可恢复 live。这个 journal 是 daemon handoff ACK 背后的 durability boundary，不是 server receive ledger
- ownership、resume safety、identity、runtime policy 与 deferred continuation 对共享 `.daemon-state.json` 的局部变更全部经过同一事务 update；temp file sync、rename 和父目录 fsync 任一步失败都必须传播。首次使用 session workdir 时还要在 ownership Put/Spawn 前把目录链逐层 fsync。Ownership 按真实 serialized JSON bytes 计量：512 live/8 MiB、512 terminal/2 MiB、1024 total/10 MiB、单条 live 512 KiB；新 MessageID 的首次 queued admission 与 oldest-live reclaim 使用同一 fsync，旧 backlog 不得造成 admission failure
- mutating request dispatch 后的 timeout/EOF/transport/decode failure 属于 ambiguous outcome，不能作为普通 retry rejection；只有 authoritative reconciliation 返回 absent 才能把 exact body 重新置为 queued
- delivery gate 串行 runtime mutation；当前 routed MessageID 是 foreground admission lane，不受普通历史 queued/inflight、restore 或 reconcile 拒绝，也不会被替换为空 launch。workflow callback authority、wake lifecycle capacity、transient recovery 与 MCP final-hop ownership 仍是 runtime mutation fence，受阻的新 owner 保持 durable queued；后台 lane 内部维持稳定选择与 bounded retry/reconcile，耗尽或 capacity reclaim 后写 failed 并产生 runtime/wake diagnostics
- Process admission lifetime context 覆盖 gate wait、render、ownership fsync、runtime RPC/raw write 和 terminal persistence；Stop/Sleep 先 non-accepting + cancel，再 drain。Runtime MCP freshness/reconcile 走 reentrant-safe lifecycle barrier，并以 per-message generation/terminal-monotonic ownership CAS 阻止旧 queued/inflight mutation 覆盖 terminal。Raw stdin 必须 inflight-before-write；明确 zero-byte 只有 durable queued 后才重试，partial/ambiguous 因无 correlated reconciler 而 terminal failed 并退休损坏的 stream/child
- unexpected Process boundary 在统一 session start reservation 下恢复 live ownership；有当前 wake 时始终 foreground handoff，不用历史 inflight 制造空 launch；无当前 wake 时才选择历史 continuation。显式 Stop/Sleep 不自动 respawn，也不在 stopping 窗口 ACK 新 handoff
- Dispatcher `Close`/`Stop` 取消并 strong-join session reservations、ownership retry、Memory watcher owners、Exec credential cleanup 与 Process callbacks，返回后禁止 late fsync/emit/publication；只有 `StopForShutdown(ctx)` bounded/cancellable：一个 caller/default 5 秒 deadline 从入口覆盖 channel、所有 session Process、callback、credential 与 owner/retry joins，各 stop/wait 并行，不能无界 WaitGroup 或按 Process 累加 timeout，deadline 后未响应取消的 cleanup 转后台 best-effort 而 API 返回，但 closed session/connector generation 仍拒绝 late publication
- trajectory GC 是 daemon-owned single epoch：Start 与 Close 原子竞争，Close/Stop 先终止 admission、cancel，再 strong-join；`StopForShutdown` 只使用同一个 overall deadline 做 bounded wait。任何持久化 mutation 前再次检查 owner context，防止 callback/periodic tick 在 shutdown 返回后写盘或发布状态
- Codex compaction 由 exact root `thread/turn/contextCompaction item` pair 观测并只标记第一条 post-boundary root usage；source 区分 `runtime_native`、idle threshold 驱动的 `agentteam_proactive` 与 terminal overflow queue 驱动的 `agentteam_emergency`。上游仍允许 `thread/status/changed idle` 先于 matching `turn/completed`；driver 先 latch idle，只有同 generation、同 compaction turn 的 completion 才消费并 emit 一次 RuntimeIdle，wrong/duplicate/stale completion 与 ordinary turn 都不能走这条捷径
- 对不支持 stdin 的 live Process，`agent:deliver` 仍可进入该 Process 自己的 durable continuation queue；没有 current accepting Process 时 daemon 拒绝 ACK，由 server fallback 到 lifecycle-owning `agent:start`，`agent:deliver` 自身不 auto-restart
- `wakeMessage` 才是“当前需要处理”的显式通知；显式 wake prompt 不展示其它 scope 的 `unreadSummary`
- `unreadSummary` 只用于没有 `wakeMessage` 的 idle/resume 未读摘要；没有新的 `wakeMessage` / routed delivery 时，agent 应保持静默，daemon prompt 不应要求 agent 主动 `read_history()`、claim task 或自行补扫别处 thread/channel

server 侧已经不再提供 agent `/receive`：

- daemon continuation inbox 只保存通过 `agent:start` / `agent:deliver` 接受、但当前 runtime 尚未消费且没有交给 ack-aware runtime pending queue 的 payload；它不向 server 补扫 unread backlog，也不暴露给 agent 主动读取。
- `agent:deliver:ack accepted=true` 是 daemon handoff ACK；server 不因此推进 read cursor，也不把历史 unread 重新投递给刚恢复在线的 agent。
- agent 需要上下文时显式调用 `read_history()` / `search_messages()`，这些读面只做 visibility/read authority 校验，不推进 agent cursor，也不变成 offline inbox。

这仍然对 DB 友好：

- agent 空闲时不会持续短轮询数据库；机器离线期间积累的旧消息不会在重连后批量唤醒 agent。
- 普通 scope human unread 仍按 member/scope cursor 与 `scope_messages.seq` / `workspace_global_seq` 计算；agent 当前事件投递按写入时 routing plan + `scope_fanout_outbox` payload/status 解释。
- 当普通消息需要更宽上下文时，通过 `read_history()` 读取该协作面的历史；non-message activities 只保存非消息 activity，不充当默认消息快照。

#### 4.8.5 人类实时更新（当前）：Socket.IO

> 当前实现：本仓库的人类端 realtime 已改为 Socket.IO（`internal/appws`，`/socket.io/*`），不再使用 SSE。当前契约以 `SPECS.md`、`docs/tech_design.md` 与实现代码为准。

当前实现要点：

- Socket.IO：`/socket.io/*`（`internal/appws`）
- transport：只接受 websocket-only（主动拒绝 polling，避免静默协议漂移；客户端应显式设置 `transports=["websocket"]`）
- 握手鉴权：Socket.IO handshake `auth` 必须带 `{ "token": "<accessToken>", "workspaceId": "<workspaceId>" }`
  - `token` 来自 `/api/auth/*` 登录/注册返回的 `accessToken`
  - server 会校验 `accessToken`、要求已验证邮箱，并校验用户属于该 `workspaceId` workspace
- 事件与 rooms 语义：以当前实现代码与本技术设计为准（HTTP `/api/*` 写操作会 emit 对应事件）
- agent activity 面板的当前实现：
  - `agent:activity` / `agent:session` / `agent:trajectory` 只投递到该 agent 的 machine owner user room；非 owner 只接收不含 current instance 的粗粒度 `agent:status`，并且 refresh/list 响应会清空 activity detail、session/current instance。
  - `agent:session` 只作为 realtime event 绑定当前 activity view；server 不把 session id 写入 TiDB 作为 activity source of truth，也不在刷新后从 DB 恢复旧 session。未上报 `agent:local-session-resume` 的旧 daemon 可使用稀疏 `agents.session_id` mirror 作为 start/resume 协议兼容，不用于 activity 面板恢复。
  - `agent:trajectory` 可携带 `sessionId`；前端按 `agent + session` 在内存保留 recent 10 条
  - 打开 activity 面板时先显示 browser/realtime recent，再接 live websocket；当没有 recent 或 daemon 尚未重新上报时，面板应显示 unknown/idle，而不是查询 TiDB runtime mirror

历史上的 SSE endpoint `GET /api/v1/workspaces/:workspaceId/events` 已随 `/api/v1` 一并移除。

#### 4.8.6 Rewind（DM / reply thread only）

当前实现新增：

- `POST /api/messages/:messageId/rewind`
- realtime 事件：`conversation:rewound`

约束：

- 只允许当前 human user 对自己发送的 hot message 发起 rewind
- surface 只允许 DM 主线或 reply thread；channel 主时间线不支持
- suffix = `[target, end]`
- 如果 suffix 包含其他 human 的消息，整个操作失败
- 是否需要恢复某个 agent，由该 agent 是否在 suffix 中“说过话”或“实际收到过 delivery”决定
- rewind support 必须按 runtime capability 判断；当前支持的 runtime 为 `codex`、`claude`、`opencode`、`kimi`，`builtin` 跟随 OpenCode；Kimi 仅支持 detached restore
- 如果 suffix 证明某个 agent 参与过，但当前已经没有可用于恢复的 live agent/runtime row，整个操作失败

当前服务端主流程：

1. 根据 target message 解析当前可见会话面，并构造 suffix
2. 校验 hot/cold 边界、other-human guard、cross-surface guard 与 runtime support matrix
3. 按每个 related agent 在 suffix 中的 `first-touch` 读取 `message_recovery_points`；若 `first-touch` 是 agent 自己发言且该消息没有对应 recovery point，则回退到同一会话里更早、最接近该 `first-touch` 的 recovery point
4. 记录 `rewind_operations(status=preparing)`，并在真正 restore 前重新加锁验证当前 suffix 没有漂移
5. 依次调用相关 runtime 的 native/compatibility restore：Codex 当前使用已 deprecated 的 app-server `thread/rollback` compatibility path（迁移到 `thread/fork` 前必须先支持新 thread-id rebind），Claude 使用 `claude_session_resume_at`（`--resume-session-at` / fresh session boundary），OpenCode/builtin 使用 `opencode_session_revert`（原生 `/session/:id/revert`）；LAN E2E 是发布前验收门槛。
6. 只有在全部 restore 成功后，才在 DB 事务里 suppress suffix 对应的未完成 `scope_activity_events` / `scope_fanout_outbox`，scrub message-bound projections，并把 `scope_messages` suffix 写成 `rewound` lifecycle tombstone；同时 clamp `scope_sync_dirty` / `member_scope_sync_dirty` 到新的 visible tail，并重算 `scopes` summary、`scope_memberships` 等派生状态；`scope_notification_settings` 是 current preference，不按 rewind suffix 修剪历史区间
7. 成功后把操作标记为 `completed` 并 emit `conversation:rewound`；该事件只作为客户端 invalidation，不创建 message、activity、runtime event 或 durable fanout row
8. 如果 restore 已成功但 DB apply 失败，把操作标记为 `repair_required`

结果约束：

- AgentTeam 对 rewind 的稳定保证是回滚 conversation state；文件、workspace、memory 或外部副作用是否会一起恢复取决于 runtime implementation
- 被删除的 suffix 不得再出现在 Web、`/api/messages/sync`、agent daemon delivery payload、Agent Runtime history / search / read-check 等读路径

### 4.9 可观测性与运维

最低配置：

- 结构化日志（JSON），至少带：`workspaceId`, `machineId`, `agentId`, `threadId`, `requestId`
- 关键指标：
  - WS 在线机器数、重连次数、断线次数
  - `agent:deliver` / `agent:start` 下发延迟、ack 失败率、daemon accepted-but-unconsumed queue 长度、唤醒次数
  - 消息写入 QPS / P95 延迟
  - `scope_messages` 当前总行数 / 总字节数
  - 归档器吞吐：每轮归档消息数、归档对象数、归档字节数、失败次数
  - 冷读比例：`history` / search/read-check 中命中冷归档的请求数（按 driver 细分：filesystem/S3）
  - rewind 指标：prepare/restore/apply 成功数、失败数、`repair_required` 数
  - `agent:trajectory`：
    - 只保留最近 N 条或最近 7 天
    - 不要无限存库

首版运维重点关注两个数：

- **hot 数据是否稳定有上界**
- **冷读比例是否确实很低**

如果这两个假设被现实打脸，再决定是否继续优化；不要在首版就预支复杂度。

### 4.10 明确不做的复杂度

首版明确不做：

- 多实例部署
- 分布式锁
- Redis pubsub
- 数据库原生 pub/sub 作为主通知通道
- 永久全量消息表
- 复杂 ACL / 每消息可见性过滤
- 消息编辑 / 任意删除 / thread reply 任意嵌套模型
- 冷数据全文搜索
- 按消息 fan-out 的 per-recipient 收件箱表

这些都不是当前问题的瓶颈；先把 append-only IM 做对。

### 4.11 Legacy Public HTTP API（已移除）

> 说明：本仓库早期曾设计过 `/api/v1/*` + SSE（`GET /api/v1/workspaces/:workspaceId/events`）的 Public API，但相关代码与路由已从仓库移除。
>
> 为保留历史背景，草案已迁移至 `docs/archive/legacy_api_v1.md`；请勿在实现中继续维护该接口。
>
> 当前实现以 `/api/*`（`internal/appapi`）+ Socket.IO（`internal/appws`）为单一事实来源；产品规范见 `SPECS.md`，代码落点见本文。

### 4.12 关键流程（端到端）

#### 4.12.1 注册与创建个人 Workspace

1. 用户通过 `POST /api/auth/register` 提交 `email`、`password`、`name`
2. 若系统尚未初始化（DB 中尚无 `users.is_admin=TRUE`），该用户会被 bootstrap 为首个 admin，并直接写入 `email_verified_at`
3. 若系统已初始化且注册策略为 `invite_only`，server 会先校验该邮箱是否命中有效 workspace invite、organization invite 或 allowlist；否则返回 403
4. server 用 `Argon2id` 生成 `password_hash` 并创建用户；注册成功后立即返回 `accessToken` + `refreshToken`
5. 若系统已初始化且配置要求邮箱验证，服务端会创建 verification token 并发邮件（mailer 未配置则注册失败）；用户通过 `POST /api/auth/verify-email` 消费 token 写入 `email_verified_at`
6. 注册/登录成功后，server 会确保个人 workspace + `#all` + human member 存在（幂等；`member.id` 由服务端生成）

#### 4.12.2 加入已有 Workspace（通过邀请）

1. workspace owner 发起邀请（`POST /api/v1/workspaces/:workspaceId/invites`）
2. 受邀人完成注册与验证
3. 登录后通过 `POST /api/auth/accept-invite` 接受邀请
4. 服务端先确保受邀人成为目标 workspace 所属 organization 的普通 member
5. 服务端在目标 workspace 下创建 human member，并写入 `workspace_role=member`
6. 默认加入 `#all`

#### 4.12.3 添加 Machine（daemon 注册）

1. owner 创建 machine，拿到 `machineApiKey`
2. 服务端直接保存 `api_key`
3. 用户在目标机器运行 daemon，连接 `/api/v1/workspaces/:workspaceId/daemon/connect?key=...`
4. daemon 发送 `ready`
5. server 在当前 hub 连接内存中记录 machine online 并 emit realtime；必要的 hostname / OS / daemon version 只作为低频 last-known diagnostic metadata 持久化

#### 4.12.4 创建 Agent

1. owner 通过 `POST /api/v1/workspaces/:workspaceId/agents` 创建 blank-binding Agent，或通过 `POST /api/v1/workspaces/:workspaceId/agents:from-role` 从 Role 创建。
2. server 创建 Agent、隐藏 member identity 与 `agent_role_bindings`；来自 preset 的请求先实现成用户拥有的 canonical Role。
3. Role package 由 `role_package_manifest_items` 表达，必需定义 `IDENTITY.md`，可附带 Skills、Knowledge、KB 与 Memory grants。
4. `PrepareAgentStart` 从 binding + manifest 生成 materialization；daemon 幂等写入 workspace。
5. runtime、model、permissionMode、auto-wake 和认证 token 都属于 Agent，不属于 Role package。
6. sibling Agent 不复制 source Agent 的 private recovery memory。

#### 4.12.5 创建 / Claim Task

1. human 或 agent 打开某个 channel 的 task board
2. 创建任务时，server 原子更新对应 `task_counters` 行，按 `next_task_number` 分配连续编号，并写入 `tasks`
3. claim/release 以幂等 desired state 更新 claimer；claim 不作为 status 权限 gate
4. status 与 Task properties 通过同一 Task update contract 更新，并生成 typed activity
5. committed Task changes 通过 `runtime.task_created`、`runtime.task_updated`、`runtime.task_deleted` live events 通知 Web；agent 通过六个 Task tools 读取和更新最新状态

#### 4.12.6 人类发消息 → 写热层 → 唤醒 agent

1. human 向 `#channel` 发送消息
2. server 在事务内：
   - `scopes.latest_seq++`
   - 写 `scope_messages`
   - 推进发送者自己的 `last_read_seq`
3. 事务提交后：
   - 若目标 agent `auto_wake_enabled=false`：只记录本次 message 可读性 / fanout diagnostics，不发控制消息，也不在恢复在线后补唤醒历史 backlog
   - 若目标 agent `auto_wake_enabled=true` 且当前 daemon 连接内存显示该 agent 正在运行：发 `agent:deliver`
   - 若目标 agent `auto_wake_enabled=true` 且 machine 当前有 daemon WS 连接但该 agent 未运行：发 `agent:start`
4. daemon 通过 `agent:start` / `agent:deliver` 把当前 routed message 交给 runtime；active-turn 期间后续消息若 runtime 支持 ack-aware pending input，则先交给 runtime pending queue 并等 surfaced ack，否则保留在当前活进程的 daemon-local continuation inbox，随后在安全 input boundary / idle 自动重试。agent 需要更多上下文时再显式 `read_history` / search

#### 4.12.7 老消息归档到冷归档存储（filesystem / S3）

该写侧流程当前未启用。重新启用时：

1. 后台归档器找到某个 scope 的下一个完整归档块
2. 从 `scope_messages` 流式读出该固定消息块并压缩成归档对象
3. 写入到确定性的 object key（见 4.5.1）
4. DB 事务内删除对应 hot rows，并更新 `scopes.archived_until_seq`
5. 归档完成后，该块由冷归档存储负责提供历史读取

#### 4.12.8 翻到旧历史

1. 用户打开某个 channel / DM / reply thread scope 的最新页，通常只命中 `scope_messages`
2. 用户不断点“更早消息”，直到 `before <= archived_until_seq`
3. server 改为按 `scopeId + seq` 直接推导需要读取的归档对象，并按需从冷归档存储读取（filesystem/S3）
4. 返回同样的分页格式；前端不需要知道冷热层切换细节

#### 4.12.9 Workflow Builder 委托 Agent 编辑

1. 每个 `workflow_plans` 行通过非空唯一 `scope_id` 关联一个同 Workspace
   的顶层 `scopes.type='workflow'`。新 Plan 在同一事务创建 Scope、初始
   Draft 和 creator membership；升级迁移仅按已有 Plan 有界回填，不扫描
   `scope_messages`。
2. Browser 打开 Plan 详情时通过 status-only endpoint 读取一次 latest durable
   wake，并复用 workspace wake cache 的 realtime 更新恢复 active edit lock；
   这条路径不读取消息。只有打开 Inspector 的 AI 编辑 Tab 后，Browser 才通过
   Workflow Plan product API 分页读取该 Scope 的消息；该 Scope 不进入普通
   conversation、Inbox、search、unread 或 scope picker。Composer 顶部把当前
   画布 selection 显示为 context attachment；空 Composer 可随 selection 切换
   附件，已有未发送文字时则固定文字与附件的关系，显式切换目标需要 Human 确认
   且不清空文字。清除附件会回到 Plan 级上下文并仍可发送，已经发送的 request
   metadata 不随后续 selection 变化。
3. Browser 发送前先通过完整 Workflow 校验和 Draft CAS 保存 source +
   layout，再向 Workflow adapter 提交 instruction、selected Agent、base
   revision 和 AST context anchor。Browser 不能提交 Scope ID、recipient、
   wake target 或受信 metadata。
4. Workflow Plan management service 在 Plan 行锁内先处理 message
   idempotency replay，再校验 Plan、Draft、target、bounded Workspace Agent
   directory candidate 和当前无 active wake。
   它稀疏确保 Human/selected Agent membership，并复用 message planner /
   applier 写一条 `scope_messages` 和一个只面向 selected Agent 的 bounded
   direct-mention `scope_fanout_outbox`。
5. active wake 由最新 Workflow Human request 的 outbox 和 durable terminal
   runtime event 推导；`pending` / `leased` / `delivered` 且没有 terminal
   event 时拒绝第二条请求。切换页面或画布不会 interrupt Agent。
6. selected Agent 只收到当前请求、base Draft revision、target 和
   delivery-only diagnostics，不获得 Workflow history/search；持久化 request
   metadata 和 delivery 都不复制 Draft source snapshot。Agent 使用现有
   `agentteam exec workflow plan` 的 `show` / `validate` / `update`。匹配 active
   Builder-edit wake 时，服务端把该 responder 的 `update` expected revision
   限定为 request baseline；第一次 changed save 后普通 Draft CAS 会拒绝再次
   使用该 baseline，Agent 不能在同一次请求里 rebase 第二次写入。该 responder
   的 `publish` / `submit` / `archive` 同样在 wake terminal 前由服务端拒绝。
   `update` 省略 layout 时在 Draft 锁事务内保留原 layout，invalid/no-op/stale/
   archived 均不增加 revision。
7. Agent 文本回复由 Workflow Plan management service 在 Plan 行锁内编排，
   通过当前 delivery 的 trusted request message identity 和 Workflow target
   调用 message-domain primitive 写回同一 Scope。事务先接受同一 client id
   的幂等 replay，再检查该 request 后是否已有 responder 可见回复，因此即使
   `visible_reply_sent` runtime event 尚未到达，也只会接受一条最终回复；
   membership 不构成 Agent history authority，也不会把过去选择过的 Agent
   加入后续 recipient。
8. 成功 Draft 写入发出 live-only
   `runtime.workflow_plan_draft_updated {planId, revision, updatedBy}`。Builder 仅在本地
   clean 时按 revision refetch；本地 dirty 时保留当前内容并显示并发冲突。
   visible-scope subscribe ACK 后精确 refetch 当前挂载的 Workflow timeline
   并重置/重放 wake snapshot，作为断线恢复边界；Plan archive 不改变
   Scope，迟到的 Agent save 在 Draft 边界失败。若归档时仍有 active request，
   Browser 基于明确的 Plan lifecycle transition 立即显示 warning toast，不解析
   runtime error 文本来推断归档。
9. 同一次页面会话内，Browser 在发送前保留已确认的 Draft source、layout 和
   revision。匹配 request 的 wake terminal 后强制 reconcile；只有 final revision
   恰为 baseline + 1、`updatedBy` 是所选 Agent 且 source 或规范化 layout 变化时，
   才在原 AI 编辑面板和原画布上显示 request-local Review。Review 只做成员、
   Workflow 配置、layout 与其他 source 的粗粒度归因；完成检查仅清除本地标记，
   撤销则以 final revision 的 ETag/CAS 写回 baseline。刷新或离开后不从消息、
   timer 或本地存储近似重建 Diff/undo。若 terminal 后 revision 未变化，原 AI
   面板直接展示未修改结果；若页面只接续到一个已经开始的 active wake，结束后仅
   确认已加载最新 Draft，并明确当前会话不具备精确 Diff/undo。

## 5. 一致性自查（与产品设计 + 当前 runtime contract 对照）

- 产品层的公开身份模型已同步到技术设计；protocol 中 human 使用 `member.id`、agent 使用 `agents.name`，Web UI 中 human-visible surface 统一显示 canonical `displayName`
- `actor` 已整体替换为 `member`
- `workspace_role` 直接存于 human member 行，成为 owner/admin/member 权限的单一事实来源
- DM 可见性以“参与者可读写”为单一事实来源，不引入额外 `visibility` 字段或专用模型
- 产品模型已区分 `Role / Agent / Instance`；Role package 与 Agent binding 承载持久化身份资产，Agent 承载 workspace actor 与 durable runtime 配置，Instance runtime state 由 daemon-local / realtime / ephemeral state 承载
- `machineApiKey` 已改为长效可 rotate 的 secret（当前仅在创建/rotate 时返回完整 key）；第一版为降低复杂度，先采用单一明文字段 `api_key`
- `agent:status` / `activity` / `session` / `trajectory` 已降级为 realtime telemetry；自动唤醒改由 server 持有的 durable `auto_wake_enabled` 决定，当前 running 判断来自 daemon WS 连接内存而不是 DB runtime mirror
- Codex runtime 已收敛为单一公开 runtime ID `codex`，其实现是长生命周期 Codex app-server JSON-RPC process；Kimi Code runtime 的公开 runtime ID 为 `kimi`，通过 `kimi --wire` Wire JSON-RPC stdio process 接入；当前 rewind-capable set 为 `codex`、`claude`、`opencode`、`kimi`、`builtin`（builtin 跟随 OpenCode），rewind eligibility 按 runtime capability 判断，而不是按 vendor label 粗略判断
- 消息模型仍以 append-only hot+cold log 为主；严格受限的 rewind 通过 `rewound` lifecycle tombstone/scrub 让 target+suffix 同步从 public/internal 读路径消失
- workspace 读文件按 `requestId` 复用，其它 workspace 操作按 machine 串行；这是受历史外部 daemon 消息形状启发的保守实现，而不是必须长期保持的一致性约束
- 明确 agent 无 join/leave channel 工具：channel membership 必须由 human / server 管理
- Task Board 提供七个 canonical agent tools：`task_list`、`task_get`、`task_create`、`task_update`、`task_set_claim`、`task_checkpoint`、`task_delete`
- Task Board 由 `internal/store/taskboard/` 的 store 与状态规则、`task_boards` / `tasks` schema、transport-neutral request/response DTO、`taskboard.ServiceContext`、browser/agent API adapter、`taskboard.TaskChangeEvent` / `TaskChangeSink` live-event contract 和 IM stream bridge 组成；运行时使用 `coredb.DB`，human taskboard 访问采用 `agentteam-core/access` 的 `channel.read|post` contract，workspace Task 聚合读取按 scope membership 过滤，agent `#channel` target 通过 core-owned `ResolveAccessibleChannelForMember(...)` 解析；web workbench 提供 direct top-level workspace `Tasks` hub、`Board / List` primary views、toolbar-level system/saved View、status/channel/assignee/other filters、sort/display controls、summary-first cards、`TaskDetailPage` + `task-detail/*` sections + compact `TaskCreateDialog`、Task discussion、Watch / Unwatch、`/tasks/settings` workspace taxonomy 管理与 per-user channel view-state；Browser 的 assignee views 由 filters / saved slices 承载，Review Request facts 保留在 task store/service，但不在当前 Browser 或 Agent mutation surface 中，messaging / identity / ID 通用规则位于 `agentteam-core/domain` 与 `internal/core/identifiers`
- 明确消息架构不再假设“全量历史都在 TiDB”，而是采用 **TiDB 热层 + 冷归档对象（Archive Storage：filesystem / S3）**，且不再依赖 `message_segments` 这类冷层对象索引表；随机 `message_id` / reply-thread short id lookup 只保留 `scope_archived_messages` 窄定位索引
