# @dsh-enhanced/assistant-delivery

本地安装初始化可使用 `ensurePrincipalLocally({ databasePath, principal })`：只在空库创建 owner，已有相同 active owner 时原样返回；不同身份、linked 或 revoked 均拒绝。它不具有 `pairPrincipalLocally()` 的 owner 交接副作用，适合可重复安装流程；显式交接继续使用原 API。

`assistant-delivery` 是个人助理唯一的、与厂商无关的消息核心。它把外部身份、一次性配对、conversation→DSH session 绑定、持久 Inbox/Outbox、adapter 生命周期、receipt、重试、对账和 dead letter 放在一个可审计边界内；飞书、Telegram 等包只实现协议适配。

本包不声明 exactly-once。它保证发送前先持久化 intent；若平台可能已经接收但响应丢失，则保留 `unknown_after_send`，只走 adapter 对账或显式 operator 决策，不盲目重发。

## 兼容性与安装顺序

- DSH / Agent / Agent Presets / LLM / Session：`0.2.1-rc.2`
- `@deepseek-ai/dsh-attachment`：`0.2.1-rc.2`，仅图片入站路径需要的可选 Host service
- `@deepseek-ai/dsh-user-questions`：`0.2.1-rc.2`。它定义 `ctx.userQuestions.ask()` 和 Agent-scoped `user-questions/request` answerer waterfall；Web Host 通过 Remote Events 在 live root Agent scope 提供 answerer。该 seam 没有可供 Delivery 消费的独立 question request/answer 事件流。
- `@deepseek-ai/dsh-commands`：`0.2.1-rc.2`，可选 Host command service；按当前命令语法/执行契约只委托安全的原生 `/compact`。`/help`、`/status`、`/session`、`/new`、`/clear`、`/stop`、`/feedback`、`/learning` 及模型/权限控制命令都由 Delivery 自有；其他原生命令即使被宿主发布也不委托、不进入 LLM
- `@deepseek-ai/dsh-goal`：`0.2.1-rc.2`，可选原生目标服务；启用 `agentGoalContinuationTimeoutMs` 时还须由 Host 挂载同版本 `dsh-goal-round-driver`，仅安装服务不产生自动续跑
- `@deepseek-ai/dsh-permission-presets` / `@deepseek-ai/dsh-sandbox-policy` / `@deepseek-ai/dsh-user-approval`：`0.2.1-rc.2`，权限档位命令使用 preset service，并通过 sandbox/approval 包的 canonical setter 固化执行事实；所需 Host service 缺失时命令 fail closed
- Cordis：`^4.0.1`
- `@dsh-enhanced/assistant-policy`：`>=0.1.43 <0.2.0`，硬依赖
- Preference Learning 是反向订阅 `subscribePreferenceFeedback()` 的可选下游；Delivery 不声明对它的 peer/runtime 依赖，避免消息核心与学习插件形成双向包依赖
- 真实多轮恢复还要求 profile 已配置官方 `ctx.sessionPersistence` provider；缺失时新建空 session 可以成功，但冷 resume 会进入有界失败/死信路径。

```sh
dsh plugin --profile headless add @dsh-enhanced/assistant-policy
dsh plugin --profile headless add @dsh-enhanced/assistant-delivery
dsh --profile headless --dump-config
```

标准 DSH `base` patch 已挂载 `@deepseek-ai/dsh-attachment-local`，因此由标准 profile 组合出来的 `web` / `headless` 部署无需重复安装 AttachmentStore；图片对象会以 content-addressed 形式保存在 `$DSH_HOME/attachments/v1`。如果自定义 profile 绕过了标准 `base`，必须自行挂载一个提供 `ctx.attachments` 的 `@deepseek-ai/dsh-attachment` 实现。服务缺失时只有图片路径会以 `attachment-store-unavailable` fail closed：不会调用渠道下载、不会把 provider reference 或伪占位文字交给 Agent，也不会退化为内存附件；普通文本仍按原路径工作。

当前 AttachmentStore 没有 reference-aware GC，也没有按 principal、时间窗口或全局容量实施磁盘配额。下载保存成功后若进程在 session 引用持久化前崩溃或授权被撤销，可能留下不可达的 content-addressed 对象；生产部署必须为该目录设置文件系统配额和容量监控，并由运维在能够证明无会话引用后清理孤儿对象。

Patch 通过 `inject: [assistantPolicy]` 固定加载策略依赖；入站 Agent runtime 另外等待 `agents`、`sessions` 与 `llm` 就绪。Web profile 提供 `agentPresets` 时会解析并挂载 session preset；不带 roster 的 headless profile 保留宿主全局工具组合。`schedulerEnabled` 默认开启，因此生产环境必须由 launchd、systemd 或 Docker restart policy 监管 DSH 进程。

Host-only `validateOwnerAgentForRoute(agent, authorityId)` 为有限动态工具提供当前调用者证明：核对原生 Agent/Session 注册对象、当前 owner route、principal record/version、workspace 和 preset；伪造对象、旧 Session 或撤权身份拒绝。它只证明当前调用身份，不授予动作权限，也不替代最终提交时的来源与 canonical writer fence。此 API 无数据库迁移，消费者须检测同批实现。

Host-only `inspectOwnerForegroundToolCall({ agent, authorityId, callId, toolName, argumentsJson })` 另核对当前认领的具体 Inbox、尚未结束的原生 turn、pending 前台执行记录及唯一未结算的 `tool/call` 事件。工具名和实际参数须与事件一致；返回内容无关的 task/owner/binding、turn、调用事件与参数摘要，不返回任务正文或参数。同 Session、开放 turn 或 owner 身份本身不足以产生证明；Host 直接调用、PTC 子调用、旧或重复事件保持未归因。此只读接口不迁移数据库，不授予执行权限，也不证明任务成功；消费者须保留后续 canonical 反馈与独立验收边界。

Host-only `inspectOwnerRequestedChange(execution, { sourceId })` 限定当前认证 owner 主 Agent 的未结 `memory_manage` 或 `wiki_upsert` 原生调用。它复核已认领的原 Inbox、当前 turn、实际工具名与参数（最多 64 KiB）、owner lineage 和 binding 代次，返回 `prepareAgentApproval` 的精确 route、Inbox 原始正文及只含身份与摘要的 SHA256 receipt。空正文、附件单独输入、PTC、历史或已结算调用均无证明。正文可能包含引用文字；哪些内容构成 owner 指令仍须由 Policy 隔离审查。原生 Web 输入在派发时登记同一前台执行记录并在 drain 时结算，失败或取消不留待结记录。该只读接口不要求 RSI `ownerRoutes` 配置、不注册 Agent 工具，也不授权写入。

## 配置

| 字段 | 默认值 | 含义 |
| --- | --- | --- |
| `databasePath` | `$DSH_HOME/assistant-delivery/state.sqlite` | 私有 SQLite 权威账本，必须为绝对路径 |
| `spoolPath` | `$DSH_HOME/assistant-delivery/spool` | 预留的私有附件隔离目录，必须为绝对路径 |
| `schedulerEnabled` | `true` | 启用 Inbox/Outbox 后台 pump |
| `tickIntervalMs` | `1000` | pump 周期 |
| `leaseMs` | `30000` | 单次 Inbox/Outbox/模型确认 claim 租约，也是模型确认实时解析的 deadline；运行中的 Inbox turn 与 Outbox send 都会周期续租 |
| `maxAttempts` | `5` | 发送与 unknown 对账各自的自动尝试上限；owner 显式 retry 会在不重置审计计数的前提下再授予一次尝试；无对账能力的 unknown 保持待人工处理 |
| `maxConcurrency` | `4` | 跨 owner scope 并发数；同一 durable principal row + workspace + preset 即使有多个 binding 也按 Inbox admission 顺序串行 |
| `maxTextBytes` | `65536` | 单条入站/出站正文 UTF-8 上限 |
| `retryBaseMs` / `retryMaxMs` | `1000` / `300000` | 指数退避边界；adapter 的 Retry-After 是下限 |
| `pairingTtlMs` / `pairingMaxAttempts` | `600000` / `5` | 一次性本地配对时效与尝试上限 |
| `defaultWorkspace` | `$DSH_HOME/assistant-workspace` | 新 session 的绝对工作区；fresh create 前自动创建缺失目录 |
| `defaultAgentPreset` | `standard` | 新 session 解析并挂载的 preset；该内置 preset 提供 Bash、文件、检索、Skills 等完整编码能力 |
| `policyRef` | `owner-dm` | 固化在 binding 上的策略引用标签 |
| `agentProvider` / `agentModel` | `deepseek-official` / `deepseek-v4-flash` | 渠道 Agent 的部署默认模型；会话可用 `/model` 覆盖 |
| `agentMaxOutputTokens` | `8192` | 单轮模型输出上限 |
| `agentMaxAutoContinuationTurns` | `2` | 截断、空响应或超出渠道正文预算时的后台自动恢复轮数；范围 `0`–`8`，设为 `0` 可关闭 |
| `agentGoalContinuationTimeoutMs` | `0` | 在原始前台回复已入队后，保留同一 Agent 等待当前已 armed 的原生目标继续执行的时间上限；范围 `0`–`300000` 毫秒，默认关闭，需 Host 挂载原生 GoalService 与 goal-round-driver |
| `agentTaskTimeoutMs` | `1800000` | 每个入站前台执行的总时间上限，范围 `1000`–`86400000` 毫秒；超时取消原生 Agent 并结束等待，未确认的操作保持 unknown，不自动重放 |
| `modelPickerTtlMs` | `900000` | `/model` 选择卡片的签名提交有效期；范围 1 分钟至 24 小时 |
| `permissionPickerTtlMs` | `900000` | `/permissions` 三档权限卡片的签名提交有效期；范围 1 分钟至 24 小时 |
| `toolApprovalTtlMs` | `300000` | 当前 open turn 的即时工具审批有效期；范围 1 秒至 5 分钟 |
| `toolApprovalPreparationTimeoutMs` | `10000` | 卡片前持久化准备上限；范围 100–30000 毫秒，且不超过即时审批总有效期；超时不出卡、不执行 |
| `ownerRoutes` | `[]` | Host-owned 稳定 owner 路由；每项精确固定 id、conversation、principal、绝对 workspace、preset、policyRef 和 minimumGeneration |

所有可变部署值都经过 Schemastery 校验。每个 binding 会持久固定 workspace 与已解析的 preset id；有 roster 时，fresh create 和 cold resume 都在 Agent 发布前挂载该 preset，不能把 session header 中的 `agentPreset` 当成已经完成组合。fresh create 的新 workspace 使用 `0700` 请求模式，已有目录的权限不会被改写；cold resume 只验证持久 workspace 仍是目录，不会在仓库被删除或网络盘掉线时悄悄创建一个空目录。SQLite 使用 `WAL`、`synchronous=FULL`、foreign keys、busy timeout 和 forward-only schema；私有目录/数据库权限分别收窄为 `0700` / `0600`，未来 schema 会拒绝打开。

## Policy 最小示例

默认无规则即全部拒绝。下面只是字段示意，请把 account、tenant、user、workspace 和 automation id 收窄到自己的真实值：

```yaml
rules:
  - id: local-pairing
    effect: allow
    subject: { kind: external, id: "local:owner-cli" }
    actions: [pair.issue, pair.link, delivery.resolve]
    resource: { kind: message, id: "*" }
    context: { initiators: [foreground] }
  - id: owner-lark-ingress
    effect: allow
    subject: { kind: external, id: "lark/my-bot/my-tenant/my-user" }
    actions: [pair.confirm, ingest, approval.decide]
    resource: { kind: message, id: "*" }
    context: { initiators: [external] }
  - id: owner-channel-reply
    effect: allow
    subject: { kind: agent, id: standard, workspace: "/absolute/workspace" }
    actions: [reply]
    resource: { kind: message, id: "*" }
    context: { initiators: [external] }
  - id: owner-channel-bash
    effect: allow
    subject: { kind: agent, id: standard, workspace: "/absolute/workspace" }
    actions: [execute]
    resource: { kind: tool, id: bash }
    context: { initiators: [external] }
  - id: supervised-growth-owner-route
    effect: allow
    subject:
      kind: background
      id: growth-supervisor
      workspace: "/absolute/workspace"
      principal: "lark/my-bot/my-tenant/my-user"
    actions: [send]
    resource: { kind: message, id: "route:supervised-growth-owner" }
    context: { initiators: [background] }
```

能力挂载、Policy 可达性和执行审查是分层的门：preset mount 决定 `bash`、`read`、`glob`、`grep` 等工具是否进入 Agent 目录，Policy 再按主体与 initiator 授权。安装器默认为本地 Web/direct `foreground` Agent 写入跨 preset/workspace 的通用 capability 规则，并为 Delivery 写入精确 preset + 绝对 workspace + canonical external principal + `external` 规则；Delivery 在每次 create/resume 时从 binding/envelope 绑定同一 principal。通配 action/resource 同时覆盖动态工具和 `memory.search`、`wiki.read`、`automation.propose` 等插件内部二次 Policy 动作，而 `background` 不会因此放宽。自定义部署可使用更窄的 allowlist 或显式 deny，deny 始终优先。message ingress/reply 使用 `resource.id: "*"` 是预期行为。`policyRef` 目前只是 binding 上的审计标签，不会把规则自动收窄为 DM；同一 owner principal 在群内 @ 机器人时仍会使用其精确 external Agent 规则。

`issuePairing()`、`linkPrincipal()`、`resolveDeadLetter()` 是可信本地控制面 API，不注册为模型工具。配对 code 只返回一次，数据库只保存随机 salt 的 scrypt hash；空 allowlist 不会把第一位来信者设成 owner。跨平台 principal 必须分别配对后再由 owner 显式 link。

### 稳定 owner 路由

长期运行的 supervised-growth / recovery 控制面不应把某一代 `bindingId` 当作永久收件地址。部署方可以在 Delivery 配置中声明稳定但不扩权的 Host authority：

```yaml
ownerRoutes:
  - id: supervised-growth-owner
    conversation:
      channel: lark
      account: my-bot
      tenant: my-tenant
      kind: dm
      chat: oc_owner
    principal:
      channel: lark
      account: my-bot
      tenant: my-tenant
      user: my-user
    workspace: /absolute/workspace
    agentPreset: personal-assistant
    policyRef: owner-dm
    minimumGeneration: 1
```

`resolveOwnerRoute(id)` 每次从 binding ledger 读取该 conversation 的当前 active binding，只接受 active owner，且要求 principal、workspace、agent preset、policyRef 全部逐项相等、generation 不低于显式 floor；`/new` 产生的同 lineage 下一代可单调前进，任一身份或作用域漂移都会 fail closed。authority 只存在于 Host 配置和 service API，不注册为 Agent 工具、不进入 prompt，也不能由成长系统自行创建或修改。

`inspectOwnerModelSelection()` 在同一 owner route 校验后提供模型快照，供修复与成长继承。传入 `sourceAgent` 时读取匹配 binding 的 Session 最新原生 `request/header`，避免初始 Agent options 已过期；未传入时读取外部会话的持久模型选择或其 Delivery 默认配置。原生 Web 的模型由 DSH 管理，必须提供来源 Agent；该接口不替它猜测模型。返回值仅含 provider/model/可选 reasoning effort，不含会话内容或凭据。

Host-only `listOwnerForegroundTaskSources({ authorityId, principalId, workspace, agentPreset, expectedOwner, after?, limit? })` 提供普通 owner 前台任务的持久完成来源。`inspectOwnerForegroundTaskSource({ ...scope, inboxId })` 在同一 owner writer 事务中精确读取该 Inbox 的 terminal 元数据，不扫描完成 feed；pending 或不合格来源返回 `undefined`，即使普通回复 Outbox 尚未入库也可返回元数据，但不提供原文或推断业务结果。`expectedOwner` 必须包含当前 `authorityHash`、`principalRecordId`、`principalVersion`；接口逐次重验 active owner、精确 route 和最低 generation。schema v25 在原 execution 行保存 `completion_sequence`，由同一 SQLite 事务的 trigger 推进 singleton clock，兼容迁移前已打开的 writer；迁移只对已有 terminal 行确定性编号，不从旧 acceptance 推断普通任务，也不制造 quiescent。完成顺序与 Inbox admission、毫秒时间及当前投递状态无关，因此晚完成的旧任务、重启和双连接不会因输入顺序漏项。页含内容空的 `unknown` 元数据；默认 50 行，最多 100 行，按 `hasMore`/`nextCursor` 继续扫描，不合格来源也会推进扫描边界。

cursor 的 protocol、数据库 epoch、稳定 owner scopeKey 和 sequence 必须匹配，越过当前 watermark 会拒绝。`/new` 沿用历史 binding 的 revoke + 1 规则，保留原 Session/source digest 和 owner cursor；owner 撤销、ABA、authority 或 generation floor 改变会拒绝旧凭据。source digest 绑定原 execution、owner、binding、Session、admission 和 envelope hash，不绑定当前 Inbox/Outbox 状态。合法 terminal execution 与 model 不可改写；直接 SQL 修改原 identity、完成结果或序号也会拒绝。

`readOwnerForegroundTaskSource({ ...scope, inboxId, expectedSourceDigest, maxInputBytes?, maxReplyBytes? })` 从同一事务快照返回 `source`、source/content digests，以及原 Inbox 输入与 exact `inbound:<inboxId>:reply` Outbox 的文本。只接受 succeeded、quiescent、frozen-model、精确 binding/reply target 的普通回复；审批、模型/权限 picker、带内部 metadata 的回复均不可读。完成可能早于 Inbox processed、Outbox accepted，甚至早于 reply 入库；内容暂不可读时返回 `undefined`，消费者需保留自己的重试。每段默认最多 16 KiB UTF-8 字节，调用方只能收窄；前缀不切断字符，并返回 `fullTextDigest`、`returnedBytes`、`fullBytes`、`truncated`，reply 另带 `outboxId`/`intentDigest`。不返回完整 envelope、内部 metadata 或附件内容。

`withOwnerForegroundTaskSourceFence(scopeAndReadInput, callback)` 在 Delivery `BEGIN IMMEDIATE` 中重验相同来源，并保持 writer fence 到同步 callback 返回；来源缺失或改变会抛错，callback 异常、Promise 或 thenable 会拒绝并回滚 Delivery 事务。消费者可在 callback 内按 Delivery → Evaluation → Memory 的锁序获取下游 writer fence，不能跨 `await`。这些接口不注册为模型工具、不新增正文或投影账本；原文本、执行成功与投递成功都不是业务目标达成或记忆真实性的证明。

`withOwnerForegroundTaskSourcesFence({ ...scope, sources }, callback)` 对最多 1000 个不重复 `inboxId` 按输入顺序，在同一个 Delivery `BEGIN IMMEDIATE` 与当前 owner 校验下读取原 Inbox/Outbox；每项提供 `inboxId` 与 `expectedSourceDigest`。空数组仍校验 owner。来源缺失、变化或内容暂不可读时对应位置为 `undefined`，不取消其他项；输入错误和 owner/authority 变化则拒绝整批。callback 收到冻结数组，必须同步结束，异常、Promise 或 thenable 会回滚；每段正文固定最多 16 KiB，调用方不能扩限。它用于在检索/快照前同时核对多个自动学习来源，并保持 Delivery → Evaluation → Memory 锁序，避免逐条 fence 的嵌套事务。

`enqueueBackgroundRoute()` 的 Policy resource 固定为精确的 `message/route:<authorityId>`，subject 同时带配置中的 workspace 与 canonical owner principal；部署规则不需要、也不应为这条控制路径授予 message 通配符。Policy 通过后，Delivery 会在同一个 `BEGIN IMMEDIATE` 事务内重新解析 active binding 并写 Outbox，封住授权期间 `/new` 的竞态。claim 时还会在同一写事务内重新验证 authority、source hash、完整 owner scope、generation floor 与当前 exact Policy；route 被删除、修改或撤权时不会调用 adapter，而是写入带 `owner-route-*` failure code 的 terminal Outbox 和 `outbox_attempts` 审计 receipt。

显式 Policy deny 表示 T3 撤权并永久 dead；Policy 检查本身抛错（例如 SQLite busy、滚动部署中的短暂不可用）不等同撤权。claim 前发生时，消息保持 `retry_wait`、`attemptCount` 不增加并暴露 `owner-route-policy-check-failed`；claim 后、adapter I/O 前发生时，当前 not-sent attempt 进入带退避的 `retry_wait`。两者都不会调用 provider，Policy 恢复后可自动继续。锁顺序也保持单向：enqueue 的 Policy 写在进入 Delivery 事务前已经完成；claim 只有 Delivery→Policy 的同步检查，没有持有 Policy 写事务再进入 Delivery 的反向路径。运维可用 `pendingOutbox` 加该 failure code 定位持续锁冲突，而不丢失唯一告警。

每条 route Outbox 的 immutable metadata 都保存 authority/source identity、authority/source hash、最初与当前解析出的 binding id/version/generation、minimumGeneration 与 receipt version，供 incident 按原 lineage 和实际 dispatch generation 对账。相同 idempotency key、authority、source、正文与格式在 `/new` 或 Host 重启后仍返回原 Outbox；修改任何不可变内容都会冲突。

若尚未发生 provider I/O，`pending` 或由 provider 明确证明 not-sent 的 `retry_wait` 可以在 claim 前用 CAS 单调重绑定到同身份、同 scope 的 `/new` generation；intent、route snapshot/hash 与 lane 会一起原子更新，最初 lineage 保持不变。claim 后、真正调用 adapter 前还会再次核对 authority 与 Policy；刚好在 claim 后提交的同 scope `/new` 可继续向稳定 conversation 发送，任何 principal/workspace/preset/policyRef 漂移仍失败关闭。已有 `attempting`/accepted/ambiguous 证据的记录绝不通过该路径迁移。

如果原记录已经是 `unknown_after_send`，重放只返回该 ambiguous 记录，不迁移到新 generation、不重新入队、更不会自动重发；稳定 route 的 operator `retry` 也会被拒绝。它只能在原 lineage 上由 adapter reconcile、由 owner cancel/park，或被后到的 provider receipt 单调结算。这是稳定 route 比普通 Outbox 更严格的 unknown-send 契约。

不同 Host 必须部署同一份 authority 配置。同一 idempotency key 的配置漂移会由 authority hash receipt 拒绝；本功能刻意不增加数据库 schema，因此使用不同新 key 的两个 Host 不会通过账本互相证明配置一致，部署层仍需用同一受控 profile 发布并校验配置。

`pairPrincipalLocally()` 是给安装向导使用的更窄离线入口：调用方必须已经拥有私有 Delivery SQLite 的本机文件权限，并提供完整 typed principal；函数内部以 owner 交接事务启用指定 principal 并撤销此前 active owner，只返回 principal 记录；它不是跨渠道别名关联。`lark-channel` 向导先用一次性 DM 短语确认具体 `open_id`，再调用该入口；普通 Agent、外部消息和后台任务都无法访问它。

## Adapter contract

渠道包通过 `ctx.assistantDelivery.registerAdapter()` 注册精确的 `(channel, account)`：

- `start({ accept, receipt })` 必须等 `accept()` 成功返回后才 ack/cursor；此时 event 已经持久化。
- `send(intent, signal)` 只能返回 `accepted`、可证明 `not-sent` 或 `unknown`。HTTP 200/平台 request accepted 只叫 `accepted`，不能伪装成 delivered/read。
- 429/明确 5xx 未发送应给出 `retryable` 与可选 `retryAfterMs`；永久 4xx 进入 dead。
- 请求可能已到平台但 response 丢失时返回 `unknown`。若声明 `reconcileUnknownSend` capability，必须实现对账，并使用独立的有界对账 attempt；连续无法判定达到上限后进入 dead，释放同 lane 后续消息。未声明该能力时记录保持 `unknown_after_send`，不会被领取为空对账 attempt，直到 owner 显式 retry/cancel。
- receipt 必须带同一个 channel、account、provider message id；状态只允许单调 `accepted→delivered→read`。
- `progress()` 收到的工具参数/结果仅是最多 1500 字符的展示预览，并会脱敏常见 credential 字段与 token 形态；畸形或原始输入过大的参数只产生状态标记，不回显 raw text。它不包含任何 reasoning/thinking 内容或 provider 原始错误。adapter 仍必须按当前受众决定是否展示明细，群聊等共享受众应保持 status-only。
- adapter 的 socket、timer、SDK client 和 listener 必须由 `start()` disposer 释放。

内部身份始终是结构化的 `ExternalPrincipalKey`、`ConversationRef` 与 `ConversationBinding`，不会从 `platform:chat:thread` 字符串反解析路由。DM 不允许 thread；group 入口要求渠道提供显式且稳定的 thread，可以是真实 provider root，也可以是渠道命名空间内按 principal 派生的顶层合成 lane。binding 仍固定精确 principal，任何同 conversation 的另一 principal 都会在进入 session 前 dead-letter，不能共享既有 owner 上下文。Lark 顶层群消息使用按发送者派生的合成 lane，真实回复串继续按 `root_id` 隔离。

### 跨渠道 `ask_user_question`

DSH `0.2.1-rc.2` 的 `ctx.userQuestions.ask()` 会把带 `agent` 的请求派发到该 exact live runtime root 的 Agent-scoped `user-questions/request` waterfall；随产品交付的 Web Host 通过 Remote Events 在同一 scope 注册 answerer。`dsh-user-questions` 不发布独立 request/answer 审计流，因此渠道集成必须直接注册 scoped answerer，并在其中完成 owner binding、adapter 能力、取消和回答校验。Delivery answerer 以前置顺序尝试有能力的渠道，避免先注册的远端 answerer 长时间等待而阻塞渠道；无 Delivery 路由、渠道不可用或回答畸形时继续 waterfall，已存在但失效的 owner binding 则以 `ASK_ABORTED` 失败关闭。没有 answerer 接受请求时 Host 以 `NO_PROVIDER` 失败，不会无限等待；多个 answerer 按 waterfall 组合顺序串联，当前 API 不承诺跨 UI 的竞速认领语义。

渠道自由文本只有在明确回复对应问题消息时才会结算，并要求 account、tenant、conversation、principal、binding version/generation 与当前 owner fence 全部匹配；同一路由恰好只有一个 pending question 也不会吞掉未引用的新消息。匹配回答直接恢复原来的工具调用，不写 Inbox、不作为普通新消息排队；`/stop`、`/new`、`/clear` 仍按渠道正常命令接入规则取消旧 wait（例如群聊可能仍要求 @ 机器人），问题卡的取消按钮则不依赖文字命令。问题发往原 binding 会话，不保证是私聊：若原会话是群聊，问题正文和选项对该群可见，敏感问题应在私聊中发起或改用其他交互方式。

## Agent 与工具

获批入站正文以 user-role message 写入 DSH，source 固定包含：

```json
{ "kind": "delivery", "channel": "lark", "account": "my-bot", "eventId": "...", "trust": "untrusted" }
```

受支持的图片入站不会把渠道资源 key 当作正文。Delivery 在确认 binding、Policy、当前模型明确声明 `image` input modality 且 AttachmentStore 可用后，才调用原 adapter 的窄图片下载接口；通过 store 的批量限额、格式解码和持久写入后，只把返回的 `ImageAttachmentRef` 作为 `ImageBlock` 追加到 session。下载后的字节由 AttachmentStore 管理，session 重启和 fork 复用同一个不可变引用，不会再次访问渠道。命令、文件、音频、视频和 sticker 不进入这条下载路径。

真实 principal、tenant、token 不注入 prompt。每个 Delivery SQLite 在首次创建时持久生成随机实例命名空间；新 binding 的 session id 由该命名空间 + conversation + generation 确定生成。数据库重开会复用同一命名空间和既有 binding，数据库删除重建或另一个 profile 的独立数据库则得到不同命名空间，避免共享 DSH session 日志根时串用旧上下文；已持久 binding 始终按自身 `sessionId` 冷恢复，不受后续算法变化影响。`/new` 新建下一 generation 并保留旧 session。有 roster 时，binding 中的 preset 会在每次 create/resume 时重新解析并挂载到 Agent scope，因此 Web profile 可以继续关闭全局 Bash/FS，而渠道 Agent 仍获得自己的 preset 工具；无 roster 的 headless profile则沿用宿主全局组合。lookup/create 进程内 single-flight，SQLite unique constraint 是最终冲突边界。

## 持久 Session 排他

内置 DSH 运行时在恢复已有 Session 或构造新 Session 之前取得持久排他凭证，覆盖普通对话、权限命令、会话压缩及首次创建和 `/new`。自动补全和原生目标续跑沿用该凭证。数据库 schema 19 的 `delivery_session_leases` 以 Session ID 为主键，保存原始会话/主体/工作区/preset/generation 身份、主体 record/version、持有者、单调 fencing token、期限和状态；这些字段不来自模型参数。即使进程在新 Session 已保存、binding 尚未建立时崩溃，其他主体也不能接管该遗留 Session。

Session 续约间隔取有效 lease 的约三分之一，有效期限使用 `min(leaseMs, 300000)`；不改变 Inbox/Outbox 的原配置。每次续约、模型请求及工具执行前重查数据库中的有效 token 与当前绑定/主体权限。丢失租约会取消本地 Agent；旧 Agent 即使换新 signal 也不能继续提交新模型或工具动作。首次派发前的过期 `prepared` 可重新授权并取得更大的 token；`dispatched` 超时只变为 `unknown`，不会自动接管。scope 身份保留，合法版本变化须重新核对当前授权。

其他 Host 入口恢复出的 Agent 也不能借用 Delivery 的 Session 执行：模型请求、流与工具入口查询持久归属，只有当前运行时实际接管的 Agent 对象才能使用其凭证。任意既有 binding（包括撤销后）或 construction 记录（包括已释放、尚未形成 binding 的遗留 Session）都要求凭证；服务重启不会把它们变成普通会话。查询失败拒绝执行，异步返回和流分块后重新检查。未归 Delivery 管理的普通原生 Session 保持原行为。

标准 Web Controller 打开冷会话时可能自动恢复 Agent，也可能直接借用正在运行的同一 Agent，因此浏览 Delivery 历史不等于取得继续执行权。正在持有凭证的 Agent 收到原生 direct-user 输入时，插入/领取事件会取消整份本地租约及当前任务，防止排队或 steer 的消息接着执行；正常 Delivery 消息、目标轮和自动补全继续走既有入口。取消不表示已停止所有外部动作；已派发任务按既有 Inbox 协议保存为 `dead_letter` / `processor-ambiguous`，不自动重放，也不作为成功。已证明 teardown 完成时可以释放 Session 占用，业务结果仍需独立核对。

此检查约束模型和工具执行，不代理 Web 的全部 Session 读写、原生目标控制或 owner 身份；在完整接入 owner/Policy 与会话生命周期前，不应把 Web prompt 当作 Delivery 入站入口。已经开始的操作无法由返回后的检查撤销。消息来源标签由可信 Host 入口设置，不构成对能够伪造标签的任意 Host 插件的隔离。

等待其他有效 Session 持有者的 Inbox 回到队列，不耗尽业务重试额度；每次领取仍使用新的单调 fence，attempt 历史保留。清理提前返回、挂起工具或未结束模型流时保存 `unknown`。只有同一凭证对应的真实 Agent teardown 完成、已跟踪模型流与工具执行均结束，才释放排他；不能把超时或原生 `complete` 当成 quiescent 证明。冷恢复没有原进程的清理证据，因此未知会话不自动重放。后续入站会收到核对提示；`/new` 可以独立开始新任务，旧 Session 与未知状态保留。当前没有模型可调用的“强制解除未知”入口。

内置 create/resume 现由 service 构造的同一租约管理器包装，继续使用宿主唯一 Agent factory 与调用方的 Cordis 作用域。冷加载尚未产生 Agent 时也计为在途工作；setup 发布前附着确切 Agent，并保留原生同步 commit 及前后授权重查。调用方即使只保留 Agent，作用域卸载也会取消租约并等待原生 handle 的实际清理；显式 dispose 幂等，正常清理不会提前结束外层验收。此内部生命周期原语没有开放 Web owner 授权或原生人类输入许可，现有 direct-user 护栏仍然生效。

原生 resume 在取消时可能先返回错误，底层 persistence preparation 稍后才清理，且未提供该清理结果的公开通知。因此恢复在 setup 收到 Agent 之前失败时，包装器核对原生 factory 是否仍为同一有效代次、调用方 fiber 是否仍 active，以及组合 signal 是否未取消；只有排除全部三条原生取消路径后，才按已结束的普通加载失败释放，保留状态提示与权限恢复重试。发生取消、factory 卸载/替换或无法证明这些条件时保留 `unknown`，不根据异常文本或迟到加载结束自动解锁；仍可通过 `/new` 开始独立会话。正常 setup/commit 失败在原生 Agent 回滚结束后按实际清理释放。

这是共享同一 Delivery 数据库、使用当前内置运行时的 Host 之间的执行协议，不是任意同 UID 进程、任意自定义运行时或外部动作的 OS 隔离。已经开始的不合作外部动作仍需回读或补偿。升级前应排空并停止所有旧 writer，统一使用当前版本；已打开旧数据库的旧代码不了解这个协议，不能与新 Host 混跑后宣称排他成立。插件卸载保留账本，不自动清理；执行护栏依赖运行中的 Delivery，卸载后的原生 Host 不提供该协议。Goals 的可选单次后台唤醒已使用同一排他入口，配置与限制见下文后台唤醒说明。

## 会话命令与持久上下文

- `/help`：列出当前实际可用的命令。
- `/status`（别名 `/session`）：显示 session 代次与指纹、上下文消息数、已记录轮次和当前模型。
- `/new`（别名 `/clear`）：停止当前任务并切换到空白的下一 generation；旧 session 及历史不删除。
- `/stop`：停止当前任务，保留当前 session 与已持久的上下文。
- `/compact`：仅在当前 DSH command service/preset 实际发布时出现；通过原生命令面执行，不交给模型。
- `/feedback`：提交固定枚举的响应偏好，或对 exact Automation 结果提交任务目标状态；在 Delivery 本地处理，不恢复 Agent、不交给模型。
- `/learning`：查看状态或 content-free T1 摘要、导出版本化稳定 JSON、暂停、恢复、按 exact T1 key 回滚或清除当前 workspace + preset 的偏好学习；只允许 exact active owner，在 Delivery 本地处理，不恢复 Agent、不交给模型。

渠道 command envelope 只接受从正文第一字节开始的小写 ASCII slash 语法。未知命令、当前 preset 未发布的命令，以及大写、空命令等非法 slash 形态都只返回确定性帮助/错误，绝不作为自然语言进入 LLM。普通 Agent turn 只有在 session persistence `flush()` 明确返回 `true` 后才入队最终回复；返回 `false` 或抛错时不宣称任务成功。

Agent Loop 以 `max-tokens` 结束、正常结束却没有正文，或完整正文超过 `maxTextBytes` 时，Delivery 默认在同一 session 中发起最多两轮后台恢复：先续写缺失部分；若仍未结束，最后一次机会优先根据当前请求和其后全部已有回答片段压缩为预算内的完整答案，不会把同一长会话中更早已完成的任务混入结论。空回答会重新生成，首轮即超长的回答会直接压缩。每个片段先持久化，再开始下一轮；恢复轮沿用已经选定的 provider、model 和 effort，但 source 明确标记为 Delivery 的内部 notice。恢复轮禁止工具执行：DSH `0.2.1-rc.2` 仍可能把 scoped tool schema 序列化给 provider，但任何调用都会在审批和执行前被拒绝，并立即结束该恢复轮；工具状态也不会展示给用户，后续恢复仍受同一总轮数限制。最终只投递一次合并后的完整回复并标记完成，不需要用户发送”继续”。
后台恢复是新的、有界模型 turn，可能产生额外模型用量，不是对同一个已提交请求的透明重试。若恢复轮失败或次数耗尽、没有新增正文，或已经持久化但无法建立安全调度边界，Delivery 才保留当前最佳正文、附加明确的未完成提示并标记失败；完全没有可用正文时发送重试提示。失败提示不再要求用户发送“继续”这类协议词。若任何片段无法确认持久化，则不投递该结果，也不宣称完成。这些失败回复不会进入 completed-turn preference projection。`agentMaxAutoContinuationTurns` 设为 `0` 时保留直接失败提示行为。

`agentGoalContinuationTimeoutMs` 默认是 `0`，保持既有的前台回复后立即释放 Agent 行为。设为正值时，Delivery 先完成并入队原始前台回复，再保留同一 Session、Agent 与已固定的 GoalId，让 Host 的原生 `goal-round-driver` 在其自己的调度边界续跑；Delivery 不创建目标轮、不接管轮次，累计上限仍由 DSH 的 `maxGoalRounds` 限制。原回复只是该入站任务的结果，绝不表示业务目标已经达成。

续跑占用当前任务的串行位置，可能产生额外模型和工具用量；时间上限不是 token 或费用硬上限。每个目标模型步骤以及工具执行前都会重查当前入站授权。目标自然停止前以及实际 teardown 完成后同样重查授权和取消信号；读取失败、替换 GoalId、超时、disposer 失败、`/stop`、`/new` 或授权撤销均记录为 non-quiescent 的未知结果，并取消该目标的后续续跑。续跑的剩余时间上限也约束 Agent teardown：超时和取消不会等待不响应取消的工具、`whenIdle()` 或 disposer；即使 Delivery 已返回，护栏仍保留到异步 Agent disposer 实际 settle（成功或失败），防止迟到的模型或工具动作越过撤权边界。该配置不提供跨日后台调度、跨 Session 目标迁移或独立的目标达成验收。

Goals 的单次计划恢复使用单独的 Host capability。`goalWakeSettlementVersion()` 返回 1 时，`resumeScheduledGoal()` 要求 capability 提供 `settle(agent, signal)`，在原生终态和 idle 后等待当前 Agent 的独立验收，再重查 owner、Session、期限和最终状态后释放租约。缺少等待协议、结算失败或取消都不能报告成功；该能力版本不表示业务目标已经达成。

`goalWakeResultVersion()` 返回 1 表示支持显式 `includeOutput`：只回读本次恢复中 exact Goal/revision 原生回合的完整最终文本，不返回历史回复、reasoning、截断或未完成工具调用。Goals 的可选事件等待在结算后调用 `enqueueScheduledGoalResult(capability)`，由 Delivery 使用自身保存的输出并在入队边界复核原 owner、binding version/generation 和 Session；调用者不能指定替代正文或收件人。Web 原生会话已在真实 teardown 前持久保存回复；当前 Web owner capability 再次确认同一 Session 后返回 `native-session`，不追加重复回复或生成 Outbox，也不声称用户已读。缺少当前 Web owner 或其身份不匹配时拒绝。外部渠道投递使用现有持久 Outbox、稳定身份 key，以及 `background:assistant-goals-wake/v1` 的 message `send` 权限。入队成功不等于渠道送达，未知发送仍不盲目重放。

外部渠道的 event-wait scheduled whole-goal 终态（当前 producer 为 `goal-event-wake-*`）另由 Delivery schema 20 在同一事务中写入 result Outbox 与 `delivery_goal_outcome_targets` typed sidecar；普通 `goal_schedule` wake 不外发该结果。sidecar 固定 owner route、principal record/version、binding version/generation、Session、业务 Goal、assessment、定义/profile/contract/receipt 摘要和 Goals proof digest；普通 `enqueueBackground()` 及手写 `dsh.learning.*` metadata 都不能制造这条 authority。入队及每次实际 send 前都会重新取得当前 Goals provider，以进程内 capability 重建同一 Goals/Verifier proof，并核对回执仍新鲜、active owner/route/binding/Session 未漂移及 exact background `send` Policy。provider 被替换、proof 过期、route 或 Policy 变化时，尚未发送的 intent 以 `goal-outcome-authority-revoked` 失败关闭，不调用渠道 adapter。

结果已经取得 provider message id 后，exact owner 可以直接回复该消息，沿用 `/feedback status`、`/feedback correct <version> <previous-status> <new-status>` 与 `/feedback withdraw <version> <previous-status>`。首次 `status` 已显示从 Verifier baseline 采纳的 revision 1；单词形式的初始判断只有与该 baseline 状态相同才幂等成功，任何不同判断都必须使用 `status` 返回的 version/status 做显式 `correct`，不能用裸命令覆盖。历史 Verifier receipt 此时允许超过原 freshness window，因为 owner 正在标注一份不可变的历史结果；但 Delivery、Goals 和 Evaluation 仍会重验同一个 sidecar、owner lineage、route/binding、Goal definition、assessment、run、profile、contract 与 receipt identity。原消息、当前 Goals provider 或任一身份不匹配时拒绝，不会仅凭过期 metadata 产生修订。Web `native-session` 返回值没有外部 Outbox/provider reply identity，因此当前不支持这条 whole-goal reply-feedback 路径。

普通前台任务也支持自然反馈：同一 owner 直接回复原任务结果，说“问题解决了”“还是不行，保存报错”或“撤回刚才的任务反馈”，Host 会记录明确判断，原文仍交给 Agent 正常续答。已有判断会自动按当前版本更正，无需先查询版本。只有明确、可确定解析的表述才记录；感谢、疑问、假设、引用、附件和没有精确回复目标的文字仍按普通聊天处理。该入口不覆盖 Automation、whole-goal 或原生 Web 结果；这些结果继续使用原有入口。

自然反馈使用 Delivery schema 24 的持久日志，绑定原始 Inbox、目标 Outbox、owner、入站顺序和一次冻结的修订参数。Evaluation 暂不可用时，普通聊天继续，日志等待后续补记；恢复只重试反馈记录，不重新运行用户的 Agent 请求。若旁路反馈日志本身未能持久化，Host 记诊断并继续普通聊天，不声称该条反馈已保存；普通派发仍须通过自身授权与持久化检查。版本冲突不自动改用新版本重试；明显早于当前反馈或无法确定顺序的相反判断不会覆盖当前结果。自然纠正原因可随精确 canonical 修订交给 Growth，最多 4096 字，作为不可信任务材料，不授予任何新权限。

`/feedback` 的完整固定语法如下；不接受附件或额外自由文本，命令本身不进入模型：

```text
/feedback helpful|not-helpful|too-long|too-short|wrong-format|wrong-action|unwanted-reminder
/feedback achieved|partial|not-achieved
/feedback status
/feedback correct <version> <previous-status> <achieved|partial|not-achieved>
/feedback withdraw <version> <previous-status>
/feedback verbosity concise|balanced|detailed
/feedback structure prose|bullets|mixed
/feedback language zh-CN|en
/feedback explanation result-first|balanced|tutorial
/feedback suggestions low|normal
/feedback ranking recency|familiarity|evidence
```

`helpful`、`not-helpful` 等只进入 Preference Learning，绝不被推断成任务成功或失败。对于
Automation 结果，`achieved`、`partial`、`not-achieved` 只接受为带固定反馈说明的、exact production
Automation 成功结果的直接回复；Automation 结果只能通过 Host-only `enqueueAutomationResult(...)` 进入
这条路径：Delivery 会重新核对 immutable run、workspace、binding 与输出 digest，并自行生成保留的
`dsh.learning.*` 证据；普通 `enqueueBackground(...)` 不能提交这些保留 metadata。同一 immutable run
的相同目标状态幂等重放为同一 Evaluation outcome，之后提交相反状态会报告冲突，不会形成第二票。

更正和撤回也必须直接回复同一条任务结果。`/feedback status` 返回当前状态、版本和可复制的命令；例如先有 `achieved` v1，可发送 `/feedback correct 1 achieved partial`，再发送 `/feedback withdraw 2 partial`。撤回后状态为 `unknown`，不会退回较早的 terminal/evaluator 成功标签；可通过 `/feedback correct 3 unknown achieved` 明确恢复。旧的三种单词命令仍仅表示初次判断，不能覆盖当前状态。

Host 从持久 Inbox、exact Outbox 和已认证 owner 的 record id/version 生成命令身份。每次接受的变化保留 append-only audit、前置版本/状态与投影 outbox；同值不加票，provider 重投返回原结果，已拒绝的乱序命令不会在稍后变成成功。冲突时应重新发送 `/feedback status` 并使用新命令。升级前的 Automation 判断可由同一 exact delivery 的 Host capability 惰性认领为 v1，不修改历史原始 outcome。

普通 Agent 回复另有一条更窄的本地 workflow 证据路径：active owner 必须精确回复该条已持久、已投递的 reply，且原始入站正文必须逐字命中已审查的闭集静态 catalog。只有 `achieved` 与该 catalog 同时成立，Delivery 才原子记录 content-free `verified-repetition` trace，并把静态模板保留在私有 registry；正文、模型输出和 owner 自由文本不会投影到 Growth。`partial`、`not-achieved`、未命中 catalog、preview、失败运行、旧 schema、手写 background intent 或不能由 durable Inbox/Outbox route 重新证明的目标只留下不可改写的本地 `no-trace` 回执，绝不宣称任务成功或形成学习样本。

workflow trace 只会投影给一个由 Growth 以 `registerWorkflowTraceSink(...)` 注册的进程内、可撤销 sink。
Delivery 先持久化 trace outbox，再异步投影并验证 exact receipt；sink 暂时不可用会退避重试，卸载、替换或
disposer 会立即撤销该 authority。这个 sink 不是模型工具、通用 Growth service 写入面或任意调用方可伪造的
callback。

只有 exact active binding 上的 active `owner` principal 可以提交。Delivery 在现有 inbound/reply Policy 通过后核对 durable Inbox/envelope、binding revision、workspace、preset 与 owner 身份，先写 `dispatch-started` fence，再复核一次身份，并对 exact external owner + workspace 执行可审计的 `signal` / `preference:<preset>/<catalog-key>` Policy 授权，最后才向唯一权威 sink 发布不可变 typed batch。`linked` principal 即使拥有普通消息入口也不能获得 `owner-authenticated`；event idempotency key 由 provider-scoped event、binding snapshot、durable principal row lineage、Inbox admission cursor 和固定 catalog selection 做 SHA-256 派生，不把 event/principal 原文传给下游。每个 Delivery SQLite 用不可变 instance epoch + 递增序号为 Inbox 建立持久全序，同毫秒事件和跨 binding 控制不依赖墙钟排序；`receivedAt` 只用于保留期，不决定控制 precedence。`too-long` / `too-short` 在同一原子 batch 中发布 T0 响应评价和对应的 T1 verbosity 选择。

注册接口是 Host-only、只读、非模型工具；`preference-projection/v2` 全进程只允许一个 authoritative sink。sink 必须将整个 batch 原子、持久、幂等地提交，并逐 event 返回 exact key + `recorded` receipt。Delivery 先把投影与 reply 写进自己的 SQLite；Preference 尚未安装、重载中或 sink 暂时失败时保留 pending/retry row，之后按原 admission cursor 重放，不重新调用 Agent。投影 outbox 以 `Delivery epoch + workspace + preset + principal row/version` 组成持久 owner lane；同 lane 较早的未终态 cursor 即使尚未到重试时间，也会阻止后续任意 preference key 和 `/learning` control 越过。control 因屏障而 defer 时保留其 durable Inbox recovery，不会把 drain 返回误报为控制成功。缺回执、错 key、重复 key或异常不会伪造下游成功；相同 provider event 与 cursor 由稳定 key 安全去重。旧 v1 或无 cursor 投影在升级边界作为全局 fail-closed 屏障，交给新 Preference 终态 ACK-ignore；无法规范化的损坏行则保留 terminal quarantine 审计后解除屏障，既不会被应用，也不会永久锁死全部学习控制。

Profile 还必须为当前 owner principal 添加精确授权，例如：`subject={kind: external, id: <canonical-owner>, workspace: <workspace>}`、`action=signal`、`resource={kind: preference, id: <preset>/*}`、`initiator=external`。没有这条规则时默认拒绝，普通 `reply` 权限不会隐式获得偏好写入权。

日常使用无需发送命令：普通 owner 文本只有在 Agent turn 完成、session 已 flush 且 exact reply Outbox durable enqueue 后，才会发布 content-free `response.language` behavioral observation。分类器只输出固定的 `zh-CN` / `en`，对短文本、代码块、URL 和混合语言会 abstain；正文和模型输出不会进入学习 event。整条短消息精确匹配“以后简短一点 / 以后用中文回答 / 今后少用列表”等持续偏好闭集（及固定英文等价）时发布 `explicit-selection`；“请用中文回答”等本轮指令不会固化，也不会贡献相反的语言证据。多行、引用、代码、长文本或仅包含短语都不会触发。下游仍须独立执行阈值、TTL、scope、CAS 与回滚门槛。

`/learning` 使用字节精确的闭集语法：`status`、`explain`、`export`、`pause`、`resume`、`rollback <exact-T1-catalog-key> confirm`、`forget confirm`。token 之间只接受一个 ASCII 空格，不做 trim 或 Unicode 空白归一化；单独发送 `forget` 只显示确认提示，不删除；命令不接受附件。`explain` 只返回 exact owner + lineage 的 catalog key/value、effect state、version 和 evidence counts；`export` 复用同一 owner/binding/lineage/admission fence 和 receipt-backed 读取，但由 Delivery 严格校验结构化 receipt 后自行生成 `dsh-preference-learning` v1 JSON。JSON 只含 current-scope T1 key/value/state/version/evidence counts，按 key/value 稳定排序，不含正文、workspace 绝对路径、principal/lineage/generation/session/event/Inbox/Outbox/cursor/idempotency/exposure，也不写文件。两种读取都不调用模型、不推进 admission high-water；相同 Inbox 重放会返回持久保存的原快照。`pause` 持久阻断新证据、T1 激活与 prompt 注入但保留已有记录；`resume` 只接受恢复命令之后收到的新事件，暂停期间已排队的 projection 会被 ACK 丢弃而不会复活；`rollback` 只 CAS 回滚该 key 当前 active T1，无 active 时是准确 no-op，两者都推进 cutoff 防止较旧 projection 复活；`forget confirm` 物理删除该 scope 的信号、假设、transition、exposure 以及较旧 status/pause/explain/export/rollback 快照，并保留最小 cutoff 防止旧 durable projection 重放。控制回执与 mutation 在同一事务提交，响应丢失或重启后以同一幂等键重放原结果。状态回复独立显示管理员 enabled gate、owner pause gate、已存 active overlays 和当前 effective overlays。

需要 resume 的命令遇到已知的持久化格式不兼容、事件类型缺失或日志损坏时，只返回有界诊断码与恢复建议，不回显 prompt/历史，也不删除原 session；可在修复对应 DSH 插件后重试，或用 `/new` 开始空白会话。

`/stop` 与 `/new` 的 cancel-and-drain 强保证目前只覆盖同一 Host 进程；在该边界内，`/new` 的 generation rotation 与命令 Inbox 已原子提交，后到消息不会夹入旧 generation。共享 SQLite 的多 Host 部署可持久 fence 尚未 dispatch 的较早 Inbox，但还没有跨 Host cancel channel，已在另一 Host dispatch 的 turn 不保证立即停止；本包不宣称跨 Host 全局 exactly-once 停止/换代。

渠道控制命令在进入 Agent 前处理，因此默认模型未安装或临时不可用时，仍可自助恢复：

```text
/model
/model use codex-subscription/default
/model reset
/permissions
/permission ask
/permission auto
/permission full confirm
```

`/model` 从宿主 `ctx.llm` 的实时 provider/model 目录生成受限的 typed `model-picker` intent，不发起模型生成；每个模型会显式关联它自己的 `effortIds`，而不是共享全局 effort 并集。支持它的渠道可以据此显示“分组、模型、effort”三级联动选择和确认按钮，不支持它的渠道会把该 intent 明确拒绝而不会降级为任意卡片 JSON。持久 selection 会先投影为不含 `updatedAt/version` 的纯路由再进入严格 intent 边界；如果目录中的异常数据仍使卡片校验失败，系统会立即退回完整纯文本目录，确定性校验错误不会重试。某个 provider 或模型能力解析失败不影响其他项，最多载入 20 个 provider、50 个模型、每模型 20 档 effort，并且全目录最多 20 个不同 effort id。

卡片提交后，Delivery 会再次核对 active binding、精确 principal/chat、原始 provider message id 和 Policy，并用实时 `resolveModelInfo()` 验证 provider/model 及该模型支持的 effort；通过后把三项选择按 canonical conversation 持久化到 Delivery SQLite。渠道可以等待该 durable settlement：同实例由完成通知即时唤醒，共享数据库的另一实例完成时由有界轮询感知，从而把原卡更新成不可交互的最终结果。选择从下一条普通消息生效，保留当前 session 上下文，跨 `/new` 和 Host 重启仍有效。卡片可选择“默认（由模型决定）”；`/model use` 保留为无卡片渠道和排错时的文字后备，`/model reset` 删除会话覆盖并恢复 `agentProvider` / `agentModel`。模型目录按宿主约定是建议性的，最终调用是否成功仍由对应 adapter 和账号认证决定。若某个已持久化的显式 effort 在下一条普通消息前被 DSH 核心判为不支持，Delivery 会在任何 prompt 或 durable dispatch 标记之前，以 version CAS 仅清除该 effort、重新预检同一 provider/model 的默认档位；CAS 冲突会重试而不会覆盖更晚的 `/model` 选择。模型不会被静默替换，也不会重放或重复计费。

`/permissions`（别名 `/permission`）只允许当前 active binding 的精确 owner 使用，命令本身不会进入 Agent 或触发 LLM。支持 typed `permission-picker` 的私聊渠道会显示“请求批准（ask）/帮我批准（auto）/完全访问权限（full）”三档卡片并标出当前档位；不支持卡片或渲染失败时自动退回含同等信息的文字命令。`ask` 使用 `workspace-write + ask + user`；当前 Host 的固定 `auto` 使用 `danger-full-access + ask + auto-review`，不会被强制折叠为 `workspace-write`。Session 格式 4 的旧动态 `workspace-write + ask` Auto 组合不满足 Policy 的 Auto 一致性检查，切换失败时持久回退 ask。`full` 使用 `danger-full-access + never + none`，可访问网络及任意文件；卡片按钮带原生风险确认，文字入口则由 `/permission full` 显示橙色警告，必须再次发送 `/permission full confirm` 才切换。

权限卡片作为受限 Outbox intent 持久化，并用独立 HMAC 域绑定原始 route、精确 owner、binding version、session、权限状态指纹、Policy 紧急停止版本、目标档位和有效期。点击时还必须匹配原卡的 provider message id，并重新核对 active owner/binding、DM、Policy 与 session；转发、错人、跨 chat、篡改、过期、`/new` 后的旧卡，以及权限状态或紧急停止版本变化后的旧卡都会明确提示失效。最终提交在权限 mutation 前、reviewer 等待后和 session flush 后重复核对紧急停止版本，因此紧急停止开→关的 ABA 也只会触发 ask 补偿，不会完成旧卡提权。同一张卡的第一次选择进入原 binding 的 durable Inbox 串行队列；点击 Toast 只表示“已受理”，真正切换成功会另发持久回复。平台重投同一选择幂等，相反选择冲突而不会晚到覆盖；需要再次切档时应重新发送 `/permissions`。

原生 DSH 设置与 Web 权限面板会写 preset、sandbox 和 approval。生产表的 `workspace-write` / `auto` / `danger-full-access` 已由 AssistantPolicy 直接解释为 `user` / `auto-review` / `none`，所以 Web 与 Delivery 可互相切档且后写者获胜，不再为 canonical 三档补 custom reviewer。旧 session 的兼容 reviewer event 仍可读，但不会把 Session 格式 4 的动态 workspace Auto 当作有效授权；非 canonical legacy full 仍经过严格迁移与 flush barrier。

旧版本中，这个缺口会产生一个很迷惑的症状：session 看似已经是 native full，但 reviewer 缺失会保守折叠成 `user`，AssistantPolicy 因而把未知/敏感工具送去 `ask-review`；同一 session 的 `approval=never` 又会在渠道审批展示前直接拒绝，上游最终误写成 `"the user rejected tool"`，所以用户既没看到弹窗也没有机会批准。迁移后精确 full 会持久补齐 `reviewer=none`；同时进度适配器会把嵌套 `tool-result.isError=true` 显示为工具失败，而不是继续显示“已完成”。

档位从 `ctx.permissionPresets.resolve()` 动态匹配：一个或两个 `workspace-write + ask` 候选仍可识别旧版共享或分离的目标，但在 Session 格式 4 上，旧动态 Auto 不会通过 Policy 一致性检查；当前 Host 的固定 `auto` 则以 `danger-full-access + ask` 独立识别。更多同类候选或多个 full 候选会失败关闭；full 候选必须唯一。目标表指纹也进入权限卡状态哈希，防止卡片发出后配置漂移。命令会显式固化 sandbox/approval；canonical 三档写官方 preset，动态/共享目标只在必要时写兼容 reviewer。mutation、flush、controller 或回复失败后会收敛并持久化到 ask；只有确认 ask 已 durable 且终态回复已经进入同一 Outbox，Inbox 才能结束，不会用“持久化结果不确定”冒充终态。

权限命令在第一次 reviewer/policy/sandbox mutation 前还必须先取得 Inbox 的 durable dispatch/recovery marker；每个异步边界后都复核 abort 与租约。若进程退出、续租失败、flush 未确认、依赖尚未就绪或回复入队失败，该 marker 会绕过普通 `maxAttempts`，在原 binding 串行 lane 中继续恢复，直到确认已持久化目标并补发成功终态，或安全收敛到 ask 后补发失败/取消终态。`/stop` 和 `/new` 会在 abort 前把正在执行的权限命令原子改标为 cancelled recovery；旧命令不会以 `processor-ambiguous` 终止，也不会从原始文本重新制造一次可能已取消的 full 提权。

普通消息真正 resume Agent 时，Delivery 先解析并挂载 binding 固定的 preset，再把该 Agent 的最终 scoped tool schemas 原样交给当前 provider/model。工具和 Skills 属于 Agent/preset，模型切换不会改变它们的可见集合，Delivery 也不维护 provider 工具白名单或部署侧能力名单。协议元数据缺失、`native` 和 `bridge` 都会放行；只有 adapter 主动声明 `toolCalls: none` 且最终 scope 非空时，才会在 prompt 发送前因“未实现统一 DSH tool-call 协议”失败关闭。这个检查描述 adapter 的传输实现，不是模型权限或能力分级；实际工具执行仍逐次经过 ToolRuntime、Policy、sandbox、审批、身份与预算检查。fresh session 创建本身不执行模型，仍可先持久建立 binding。

只注册两个模型工具：

- `delivery_reply`：只能回复当前 Agent session 已验证的 binding，不能传 channel/account/chat/user。
- `delivery_status`：只返回有界 id/status/time/failureCode，不返回正文、route 或 secret。

普通 Agent 回复、Automation 结果和未来 Heartbeat 都必须 enqueue 到同一 Outbox。相同 `idempotencyKey` + 相同不可变 intent 返回同一行；同 key 改正文或 route 会冲突。

延迟审批由 domain 先通过 `prepareAgentApproval()` 从当前 Agent 的 active owner binding 派生不可伪造的 source/binding/workspace/principal route，再把该 route 随 Policy proposal 持久化；background Agent 可由可信 runner 使用 `bindAgentApprovalRoute()` 临时绑定 automation definition 中的 immutable `deliveryBindingId`，该绑定不会注册为模型工具。Delivery 在同一 owner 事务内从 active binding 派生 canonical external `principalId`，并把它固定到私有 workflow template content/digest；Host resolver 在返回 prompt 前会再次核对 exact binding version/generation 与 principal。该 principal 不进入 Growth trace 或 Growth 数据库。Delivery tick 会在发送前领取 Policy dispatch，以 `approval-card:<proposalId>` / `approval:<proposalId>` 写入同一 Outbox，再用 CAS 标记已入队；若进程在两步之间退出，重放仍命中同一 intent。schema v6 另以带版本 fence 的持久 `(createdAt, proposalId)` 高水位分页扫描 pending dispatch：无效 route 不会被伪标为已入队，但也不能用一个 poison page 永久饿死后续审批；扫描到尾后回绕重试这些条目，Host 重启与并发 scanner 都不会倒退覆盖新游标。

原生 owner Agent 每步 prompt 的只读审批状态由 Delivery 按当前直连 active owner binding、workspace、preset 和 Policy `history` 权限重新读取；只查询 personal-memory 与 personal-wiki 两个精确 v2 route，每源最多五条。首次发现真实提案后才挂载 Agent-scoped context，普通无提案会话保持原有 prompt 装配。context 只含经过领域资源格式筛选的提案 ID、资源 ID、动作、状态、版本、时间与截断标志；不包含审批正文、摘要或 diff。owner、binding 或服务权限失效时旧状态不再输出，读取失败显示不可用。状态不是操作授权：拒绝或过期的旧卡不能继续索批；新 owner 消息再次明确请求时才可通过正常工具发起新请求。Policy 批准也不证明领域写入完成，需核实工具的 record/page 结果或当前读取；此状态不会自动重放、发送或写 Memory/Wiki。

Policy 的 `approved` 只表示 owner 决定，不表示领域变更已经应用。schema v10 的 Host-only
presentation publisher 由 Delivery 只向当前 Evolution 或 Automations 实例签发、可撤销的进程内 capability；它不是
公开 service API，也不能由普通调用方伪造。该 capability 接收领域结算或 Automation incident 生成的单调版本 typed
projection，持久等待原 Outbox 真正取得 provider message id，再通过 adapter 原位替换同一条消息。
只有带 exact rule id/version/status 的 `applied` 可以显示为已生效；`conflicted`、`expired`、
`rejected` 各自保留独立终态。更新采用 lease、fencing token、payload digest 与重启重试；新 revision
会压过旧 provider completion，撤权或原消息永久失败则明确进入 dead，而不会把“已入队”误当成“已展示”。
临时 adapter/provider 更新失败不消耗普通 Outbox 的 `maxAttempts`，会持续退避恢复；只有原消息已不可变 dead
或 authority/owner route 被撤销才终止该 presentation。

Automation incident 使用 `automation-incident:<incidentId>:g<generation>` 同时作为 presentation 与原始 Outbox identity。同 generation 的 open/detail/recovering/resolved revision 会在发送前合并到最新期望；原消息处于 attempting 或 unknown 且没有 provider id 时只等待，不猜测发送结果。取得 provider id 后，每次更新都重新验证原始 owner-route receipt、当前 authority/generation/owner 和 exact Policy；暂时检查失败或 provider update 异常会退避重试，Host 重启和失去 fence 后仍可恢复。generation+1 使用新 identity，因此真正 reopen 会创建新消息。损坏的 presentation 行会原位隔离，不能饿死后续生命周期。

`enqueueApproval()` 只接受固定的 operation/proposal/version/expiry/title/diffHash 字段，并重新核对 active owner、精确 workspace/principal、Policy pending snapshot、展示标题和正文 SHA-256，调用方不能替换审批卡片展示内容。渠道签名后的点击仍必须回到 `settleApproval()`；它从 Outbox 取回持久 operation tuple，并再次核对 active owner binding、principal/chat、proposal version/expiry/diffHash 与 Policy pending snapshot，重复点击只会得到同一持久化结果。若 Policy 已提交而 Delivery 在写回结果前退出，只有此前已经落盘的同一 settlement 且当前 Policy 为精确同一终态时才能通过 `recoverApprovalSettlement()` 补全；该恢复路径不会新建 settlement，也不会决定 pending/expired proposal。

即时工具审批通过 Policy 的 Host-only 人工渠道注册口接入原生 Approval，先完成自动审核，再认领当前渠道，最后才委派原生 Web Remote；不再使用会被更早的 Web listener 饿死的平级监听器。Delivery 与 Policy 须成套升级，人工升级状态通过当前注入的 Policy 服务查询，不依赖另一份包导入的模块私有状态；同版本包被不同路径加载时也不能误走 Web。旧 Policy 缺少 `registerHumanApprovalAnswerer` 或 `isAutoReviewEscalation` 时明确报升级错误，不能把用户会话切成 full 来规避。

即时工具审批是另一条刻意不持久化的 open-turn 路径：effective reviewer 为 `user` 时由 Delivery 交给 owner；默认 `auto-review` 下，Policy 已判定为普通的前台 Bash 与只读网络不出卡，只有本地明确敏感、原生 sandbox escalation，或未知插件工具经 reviewer 升级的 exact request 才进入人工。`never`/`none` 不创建渠道提示。只有已绑定的 Web owner 才能使用 notice-only adapter 委派原生审批；Delivery 继续 approval waterfall，由 DSH 原生 Web 控件展示批准/拒绝并恢复当前调用；不会以 `unavailable` 吞掉交互。Lark 私聊直接发送签名 CardKit 2.0 审批卡，卡片明确显示任务暂停；当前调用等待 owner 点击“允许一次”或“拒绝”，等待期间不执行工具，只有 provider 接受后的交互面才算成功；Delivery 不再提前发送“请查看卡片”的误导文字。卡片 `format_error` 可由 Lark adapter 降级为同一私聊的明确文字审批，并且只在恰好一个匹配 pending request 时接受 owner 的精确“允许/允许一次/拒绝”回复；其他发送失败保持 `unavailable`。Lark 群聊/话题触发的高影响调用不会在群内暴露 raw arguments：Delivery 仅在同一 channel/account/tenant、同一 owner principal、workspace/preset/policyRef 下恰好存在一个 active DM binding 时，把审批卡转投该私聊；执行仍绑定原群 session，批准回传后同时复核来源与展示 binding。DM 缺失或不唯一时失败关闭，不猜收件人。Delivery 同时识别当前未结束 turn 内唯一且未结算的顶层 `tool/call`，以及带 exact root/parent/sub-call identity 的 Code Mode `tool/ptc-dispatch-start`；已经出现 `tool/result` 或 `tool/ptc-dispatch` 的调用一律拒绝。raw arguments 必须完整且不超过 16 KiB，reason 必须完整且不超过 2 KiB，均不截断。reason 只是未经信任的展示说明、绝不是可执行指令，但仍作为用户看到的 exact 内容纳入授权 hash。

展示前持久化准备受 `toolApprovalPreparationTimeoutMs` 的独立短期限约束（默认 10 秒），不会耗完用户点击卡片的整个等待窗口。超时后即使旧 flush 晚到也不能派发卡片。`ctx.sessions.flush(session)` 必须返回 `true`，即至少一个持久化 listener 已成功把包含 `approval/asked` 与 exact tool/call 的事实落盘；返回 `false` 或抛错都会 `unavailable`。每次请求使用不可猜的随机 operation nonce，并以 SHA-256 `actionHash` 绑定 exact binding revision/route、owner、session incarnation、当前 turn、顶层或 Code Mode 调用身份与 raw arguments、exact reason、当前 provider/model/effort、人工 review route/escalation 与 permission/reviewer/policy 事件。flush 后、adapter 返回后都会重新核对 binding/principal/agent/delegated token/adapter 与 actionHash；撤权、解绑、route、escalation 或权限漂移都不能返回 grant。owner 返回允许后还会以不重复消耗预算的只读方式复核 Policy emergency stop；卡片等待期间打开紧急停止会把 grant 收紧为拒绝。

这类工具卡只在 `toolApprovalTtlMs` 内和当前进程的 open turn 中有效；调用方 abort 才是 `cancelled`，超时、过期、adapter 异常/断线/注销、Host 重启或 Delivery dispose 都 fail closed 为 `unavailable`。卡片未送达时只发送一条明确说明“本次不会执行”的失败提示，不会先刷一条不存在的待处理卡片通知。旧卡不恢复、不自动重放，也不进入 Outbox。它不同于上面的 durable Policy proposal：proposal 有持久 operation、CAS settlement 和受控 recovery，适合延迟审批；即时工具授权只允许同一 live call 的一次结果。

当当前前台 Inbox 与 owner 回合可独立重验时，Delivery 会经正常授权和 durable Outbox 在**请求来源会话**发送无参数的提示：私聊提示处理授权卡，群聊/话题明确提示去已配对私聊授权。DM 缺失/不唯一、卡片配置缺失或准备超时时，提示如何恢复，不会把飞书请求转进无浏览器的 Web 等待。提示只是消息，不是授权，也不能绕过发送策略。卡片本身仍只属于当前 live 调用，不持久重放。

进度消费原生 `approval/decided`，分别展示批准、拒绝、取消、不可用/超时；没有人工点击的超时不再误报为用户拒绝，也不会永久保留“正在确认权限”。

模型选择同样不开放任意渠道 payload。`model-picker` intent 只允许有界的 provider/model/effort 目录和有效的 model→effort 引用；目录与 operation 一起保存在 Outbox，渠道可在 Host 重启后按 operation 恢复同一份选择目录。schema v5 另外保存每个 operation 的选择 revision 和确认结算：渠道联动回调通过 CAS 前进，旧 revision 只能读取当前状态；确认先持久领取 operation，带租约和 fencing token 的 worker 在启动与 tick 时恢复未完成任务，并在实时模型解析后重新校验 active principal、binding 与 Policy。最终模型选择、结算结果与确认 Outbox 在同一事务中提交，因此重启、慢解析与重复回调不会重复应用选择。

## 故障语义

- Inbox：`received→authorized→queued→claimed→processed|retry_wait|dead_letter`。
- Outbox：`pending→attempting→accepted→delivered→read`，另有 `retry_wait`、`dead`、`unknown_after_send`。
- attempt 与 receipt 都是独立追加账本；不同 binding 可并行，同一 binding 保序。
- claim/complete 带 owner + fencing token；过期 worker 不能提交新 owner 的结果。
- 运行中的 Inbox/Outbox claim 每隔约三分之一租期按精确 owner + fencing token 续租；续租失败会 abort Agent/adapter signal，过期 worker 即使随后返回成功也不能提交结果。
- `maxAttempts` 只限制自动 claim；owner 对 dead letter/unknown 的显式 retry 把状态 CAS 回 `queued`/`pending` 并获得一次新 claim，`attempt_count` 与 attempt ledger 不回绕。
- schema v9 为每次 operator `cancel|retry` 写入不可变的 `dead_letter_resolutions` receipt，精确绑定 `kind + message id + attempt_count`、原状态/故障码、必填 operator 和 receipt version；消息与 attempt 审计行不会被删除。控制面返回 `{record, receipt, replayed}`，响应丢失后同一 operator 重放同一决定会在重新通过当前 Policy 后幂等返回，换 operator 或相反决定冲突；其中 receipt 指向被结算的旧 exact attempt，record 是重放时的当前消息投影，可能已经进入下一 attempt 或外部 receipt 终态。`retry` receipt 只结算旧 attempt，下一次 claim 会增加 `attempt_count`，若再次失败会重新成为 actionable。
- v8→v9 会为旧版仅以 `operator-cancelled` 标记的结算回填 legacy receipt，并从 exact attempt 恢复旧 unknown 的 ambiguous 状态。actor 无法恢复，因此明确使用 `legacy-v8-migration` sentinel；被覆盖前故障码优先从同 message + attempt number 的 attempt history 精确恢复，历史缺失或没有 failure code 时写入字面值 `legacy-unknown`，不把缺失伪装成可归责事实。迁移后数据库 trigger 要求所有新的 resolution UPDATE 先有同事务、同原状态/故障快照的 v9 receipt；因此迁移前已打开的 v8 writer 会安全失败，不能绕过 operator identity 或重新领取已取消的 unknown。共享数据库滚动部署应先排空旧 writer，旧实例出现该 fence 错误后由 v9 控制面重做明确决策。
- `health().deadLetterInbox/deadLetterOutbox/unknownOutbox` 保留为历史 terminal 行总数以兼容旧消费者；告警与健康聚合应使用 `actionableDeadLetterInbox/actionableDeadLetterOutbox/actionableUnknownOutbox`。对应 `resolved*` 字段统计当前 exact attempt 已有 cancel tombstone 的行，因此 operator 已处理的历史不会让系统永久 degraded。
- `unknown_after_send` cancel 后仍保留为独立的 ambiguous 状态和原始 resolution receipt，不伪装成确定未发送；该 exact attempt 不会再被自动 reconcile/send，并视为 lane terminal，cancel receipt 也不能改写成 retry。若平台随后给出匹配 provider message id 的 delivered/read receipt，外部事实可单调提升当前状态并清除当前失败标记，原 cancel/attempt ledger 仍保留。普通尚未结算的 unknown 只能由显式、Policy-gated operator `retry` 重新入队；稳定 owner route 禁止 retry，只能原 lineage reconcile/cancel/park。系统从不自动重发 unknown send。
- Agent dispatch 前写 `dispatch-started` marker。此后进程崩溃会进入 `dispatch-ambiguous` dead letter，避免自动产生第二个 turn；这会牺牲一次自动重放，需要 owner 审阅后显式 retry。
- 对仍绑定当前 owner 的外部普通请求，恢复扫描会在没有最终回复时持久排入一次中断说明，沿用当前入站、回复和发送前授权；历史已中断记录也会扫描。已有最终回复（包括发送结果未知）或 operator 已结算的记录不另发通知。通知不重跑 Agent、不把原执行改成成功，也不确认之前写入是否完成。
- `/permission` 是受限例外：它使用专用 commit/cancel/failure recovery marker，并以精确 Inbox id（兼容旧 event id）与 `replyToEventId` 共同证明终态 Outbox；崩溃恢复不会把普通 background Outbox 或同名伪造 key 当成完成见证。
- 当前 DSH `0.2.1-rc.2` 的 `followup()` 没有跨进程 `sourceEventId` 唯一接纳/完成 handle，因此本包诚实承诺“持久 event 去重 + at-most-once 自动 Agent dispatch”，不声称端到端 exactly-once。若宿主未来提供该 seam，可升级为安全的 at-least-once wake。
- Outbox adapter 抛异常一律视为可能已发送；不会按照普通 5xx 重试。

## 受控维护重启

普通前台任务默认有 30 分钟执行期限（`agentTaskTimeoutMs`）。期限到达后取消原生 Agent，并给合作的取消流程最多 1 秒写入原生结束事件；不响应取消的模型或工具不再无限占用 Delivery 等待。超时原因持久写入 Inbox，进度接口失败时仍通过原有 Outbox 发送幂等中断通知，不标 DONE。未确认停止的操作保持 unknown，Session lease 在真实 disposer 完成前继续阻止接管和迟到工具，不自动重放写入任务。

schema v27 持久保存渠道进度句柄及待发送终态。终态先入账，再调用渠道接口；普通退出最多等待进度 5 秒。支持此接口的适配器在重连后依据 Inbox 和执行回执收尾，不能将中断或未知结果显示为任务成功。旧版本未保存的句柄无法回填，也不能据此原位关闭历史气泡。

显式配置 `restart` 后，当前 owner route 的前台任务在首次派发前冻结 owner/binding/Session、实际模型与 reasoning effort、单次输出上限、模型/工具调用次数和绝对截止时间。`allowedTools` 只选取已由可信服务登记实际定义对象的只读工具作为恢复能力：`delivery_status`、`wiki_search`、`wiki_read`、`memory_search`。同名 scope 覆盖、提供者卸载或未登记定义不会继承资格。普通任务仅记录模型、调用量与恢复资格，原有工具、时限和模型选择不受恢复配置限制；执行写工具、委派、超额、模型变化或不明结果只会失去自动续办资格。申请恢复后才强制冻结的剩余额度、截止时间和只读工具清单。它不增加原 Policy 权限。旧任务不会被追补为可恢复任务。

`delivery_request_restart(reason)` 只接受原因，不接受 unit、路径或命令。调用须通过当前 owner 的精确 `maintenance.restart` 许可，且是该模型响应唯一的工具调用；活动 acceptance/native goal 或其他未静止的 Agent/Automations 会拒绝。原生 `concludeTurn()` 结束当前轮，工具结果和完整 Session 经过 persistence flush；实际 Agent teardown 与租约释放后才写 `ready`。schema 26 的全局维护栅栏与 Inbox claim 共享 SQLite 写事务，后台 Automations 同步暂停新领取，Outbox 仅发送本次维护通知并等待已有发送结算，已接收的控制命令仍可撤销恢复。恢复过程中原任务不被标为成功。

私有 sidecar 账本采用 WAL/FULL、CAS 和不可变预算预留。外部 `bin/dsh-restart-supervisor.js` 在安装器同名生命周期锁内检查原任务、维护通知 accepted、全部 Session lease/Automations 静止、当前 Policy 紧急停止和维护许可，以及 unit、profile、cohort receipt 和已安装代码摘要。它持久领取后至多执行一次固定的 `systemctl --user restart`；命令结果不明时只观察，不重发。新 InvocationID、Host ready、飞书连接及稳定窗口全部满足才允许续办。

新 Host 在同一 Session 和原 Inbox/foreground execution 下追加固定、稳定 ID 的 Host notice，复用原生 AgentLoop。原生 API 以 `role:user` 承载 notice，`source` 明确为 Host；它不创建新的认证入站消息或 owner 授权。Session 的 inbox-splice 和领取记录必须在模型调用前落盘；恢复不重置模型、预算和截止时间。新 Host 稳定、原 Session 已独占且续办 notice 已落盘并重验授权后，即解除全局维护暂停；其它会话和后台任务恢复正常收发，原会话仍按 Session lease 串行执行，不必等待续办任务完成。`/stop`、`/new`、撤权、模型/代码漂移及未知外部结果阻止自动重放。维护失败写幂等说明，不添加 DONE；原进度可用时追加“任务已保存”并关闭这一次 COT run；平台 `RUN_FINISHED.status=done` 只结束该进度流，原任务回执仍 pending，消息不添加 DONE reaction。历史遗失的进度句柄无法原位关闭。

冻结 owner 安装通过 `scripts/install/upgrade-program.mjs --restart-config /absolute/private.json` 增加能力：该文件须 owner-private，包含 `databasePath`（固定 Home 下 `assistant-delivery/restart.sqlite`）、`ownerRouteId`、`maxModelRequests`、`maxToolCalls`、`taskTimeoutMs`、`allowedTools`。安装事务冻结内容和摘要，精确新增稳定 owner route 的许可（每次执行仍复核当前 binding）并更新签名 overlay；恢复不重读配置输入。升级后用 `scripts/install/restart-supervisor.mjs --dry-run` 验证并以 `--install` 安装 Home 外的私有配置和 user timer/oneshot；参数见该脚本，helper 随本包发布。

监督器的 Node 入口与主 Host 一样过滤 `ExperimentalWarning`，避免每轮轮询重复打印 SQLite 实验特性提示；普通警告、错误和非零退出状态仍然保留。正常的 `idle` 表示本轮没有待处理重启请求，oneshot 成功后显示 `inactive/dead` 是预期状态，应结合 timer 和服务的 `Result`、退出码判断健康。

这是同码、事先保存的维护恢复；不提供代码升级，不恢复未知派发，不保证硬崩溃后任意指令继续，也不保证目标离线期间飞书消息不丢。独立接入进程仍是后续交付。外部 helper 与目标同 UID，不构成恶意代码的 OS 隔离；有宿主 shell 权限的模型仍须遵守既有 sandbox/Policy。每个任务最多申请一次，执行器还有时间窗重启次数上限；新建或经受管程序升级的 unit 同时限制 600 秒内最多 3 次启动，重复启动失败会停止并保留诊断，不无限抖动。同 Host 版本升级也会事务式迁移旧 unit 的启动限制，保留原启动命令和环境变量，失败回滚恢复原 unit 字节。通知或监督器在 120 秒内未接管则解除维护栅栏并报告失败；监督器重开不会重置已派发重启的观察期限。维护期间普通 `/status` 会随其它消息排队，`/stop`、`/new` 可直接撤销。额度是调用次数、单次输出和时间上限，不是精确总费用上限。

## Goals 单次唤醒边界

Delivery 只向 Goals 的已注册 Host capability 提供一次受限恢复：它重读当前 owner route、binding、原 Session、GoalId、revision 和 deadline，在重新加载原 Session 后重查状态，仅在 Goals 的 dispatch CAS 成功后恢复原生目标。它不把历史 Session snapshot、runtime context 或任意 `user/message` 当作新的 owner 输入；DSH 会把 runtime context snapshot 持久化为 user-role message，它不能替代人类回合证明。

Web 事件唤醒若仅调用工具、没有最终回复正文，但准确原目标已结算且取得新鲜的独立成功验收，Delivery 会在复核 Goals capability、owner、原会话和发送权限后显示“目标已通过独立验收并完成。”。这是有期限、按验收证明去重的原会话通知，不伪造模型回复，也不授予执行权限；发送前继续检查 owner、binding、route、Policy 和期限。已有正文沿用原生回复；无正文的非 Web 唤醒仍不能生成外部结果消息。

Cordis 按上下文读取 service 时可能产生不同 proxy 对象，Host 不能用对象 `===` 判断同一 Delivery service 或 capability 所属。恢复、撤权、lease 到期、取消和 teardown 只能阻止新的边界动作；它们不证明已有模型、工具、子进程或外部动作已经停止。无法确认终态时返回 unknown，Goals 不自动重放。

## 权限与数据边界

- **文件系统：**读写配置的 SQLite 与私有 spool 目录，并在 fresh Agent create 前创建缺失的 workspace；cold resume 只验证原目录仍存在。图片字节通过宿主 AttachmentStore 持久化；标准 `attachment-local` 将其写入 `$DSH_HOME/attachments/v1`，Delivery 自己不把字节写进 SQLite、spool 或 session 日志。Delivery 本身不读取任意 workspace 文件。挂载 preset 后，获 Policy 允许的文件工具可在 DSH 文件权限边界内读取 workspace。
- **网络：**本包自身无网络访问；网络权限属于注册的渠道 adapter。
- **进程/浏览器：**常驻 Delivery 服务不启动 subprocess、shell 或 browser；可选外部维护 helper 仅执行安装器固定的 systemd/journal/flock 命令并读取受保护的状态与代码；它挂载的 preset 可以暴露 Bash/Pwsh，真正执行由宿主 sandbox、approval 与 Policy 三层共同约束。
- **凭据：**不接收、存储或输出 provider token；adapter 必须通过 SecretRef/后续 credentials service 获取。
- **数据：**Inbox/Outbox 会保存消息正文、外部 identity 和 route，属于 PII。入站只登记最多 10 个受限 provider descriptor；授权图片会额外保存经过校验的 AttachmentStore 引用和内容摘要，provider reference 始终留在隔离账本而不进入 prompt、模型请求或工具输出。图片二进制遵循所选 AttachmentStore 的保留策略；标准 `attachment-local` 当前没有引用垃圾回收。文件、音频、视频和 sticker 仍保持 metadata quarantine。当前版本也不自动清理 Delivery 历史，请依部署的数据保留策略备份/轮换私有数据库和附件对象。

## 当前非目标

- 多主机分布式 worker、跨平台自动合并、群聊共享主 session、广播、presence/read SLA。
- 普通文件、音频、视频、sticker 的 payload 下载，OCR、病毒扫描与 multipart 附件发送；当前二进制入站能力严格限于受支持的光栅图片。
- 任意收件人模型工具、把 provider accepted 当 delivered、对未知发送自动重试。
- OS 级恶意插件隔离。它约束遵守 Cordis service contract 的代码；同进程恶意 Host 插件仍需容器或独立 runtime。

## 验证

```sh
pnpm --filter @dsh-enhanced/assistant-delivery test
pnpm --filter @dsh-enhanced/assistant-delivery typecheck
pnpm --filter @dsh-enhanced/assistant-delivery build
pnpm --dir plugins/assistant-delivery pack --dry-run
```

兼容性基线见 [docs/compatibility.md](../../docs/compatibility.md)。

普通前台与 Automation 的更正/撤回/状态入口兼容 Evaluation `owner-objective-revision/v1` 或 v2；whole-goal 入口严格要求 v2。旧 Evaluation 不能消费 typed whole-goal capability，命令明确返回服务不可用，不能退化为独立评价票。

旧 foreground receipt 未保存 principal version：仅当前仍为同一 record 的初始 v1 身份可修订；身份已更新时保守拒绝继承旧判断修订权。新记录完整保存 record+version。

Host 可用 `inspectOwnerForegroundLearningTask()` 将 Evaluation 增量结果绑定到精确 owner。它核对当前 route、原始 Inbox/binding、Host 执行回执和 principal record/version，再区分独立 Verifier 与 owner 的纠正/撤回；只有核对成功才返回来源 Session、最多 4096 字符的原任务正文及 canonical receipt；当前 owner revision 的操作身份能与自然反馈日志核对时，另返回最多 4096 字符的 `feedback.text`、来源 Inbox 与截断标志。普通 owner 反馈对应的原 Outbox 内容可读且与 canonical 引用一致时，`source.reply` 还返回最多 4096 UTF-8 字节的原答复可见文本、截断/过滤标志、完整原文摘要及 Outbox/intent 绑定。它先在现有 reader 的 16 KiB 硬上限内读取完整原答复，再用进度展示的常见凭据形态过滤；超过 16 KiB 时仅返回固定占位文本，不暴露部分前缀。`redacted` 表示文本经过滤发生变化或被占位替换，不能保证识别任意秘密；历史内容不可读时省略该字段。这里的答复是原 Agent 回复，不是 tool stdout，也不是事实 oracle 或授权。来源带 `modelSelectionState`：`frozen` 才返回实际原生 `request/header` 已物化的 provider/model/可选 reasoning effort；`missing` 表示历史执行未采到调用快照，`inconsistent` 表示同一前台执行出现不同实际路由。后两者不猜测当前会话或部署默认模型。schema v23 为内置 Delivery 运行时的普通 owner 对话增加独立执行回执：无需预设验收 profile，回复具体消息的 `/feedback not-achieved`、更正或撤回也能进入 canonical 前台结果；schema v24 另支持上述明确自然回复。记录只证明任务归属、实际模型和执行已停止，不能证明目标达成；没有验收契约时只接受已认证 owner 的明确反馈，不能取模型自评。这类新任务的目标结果反馈要求 Evaluation 可用，先提交 canonical 修订再更新本地 workflow；服务缺失时明确返回不可用，不确认仅有本地记录的成功。偏好反馈和没有执行回执的历史 workflow 路径保持原行为。旧 acceptance 回执继续兼容，旧普通任务不回填，终态执行不能事后补写路由。此入口覆盖内置外部渠道运行时；原生 Web 和自定义运行时不能据此推断已接通。`/new` 前的同 owner 任务可被识别，主体身份变更不能继承旧任务权限。该接口仅供发现前台学习来源，尚不自动调度修复，也不授予执行或采用权限；消费者写入前仍须使用 Evaluation 的 canonical writer fence。


## 原生 Web owner 接入

可选的 `@dsh-enhanced/assistant-web-owner` 消费受信 Host API `bindNativeWebOwner(ctx, config)`。每个 Delivery 服务只允许一个活动 Web Controller capability；身份必须是已配对、当前 active owner 的 `web` principal，workspace/preset 和 owner record/version 在颁发时固定。此 API 不暴露内部 lease manager，也不对任意同进程 Host 代码宣称沙箱隔离。

新 Web Session 先在同一 construction lease 下创建并 flush，再在 SQLite 事务中创建 binding 并提升该 lease 的 target；中间不会失去占用或出现第二执行者。文本 Inbox 直接原子 claimed，native-admission/native-dispatch-started 标记保证崩溃后不被普通入站 worker 重放。原生 inserted/claimed 的确切消息证明连接到既有当前 owner 回合与前台验收协议。空闲、超时、撤权或 scope 卸载调用实际 memoized disposer；未完成清理不记 quiescent。

原生 Goal 仍为 `active` 且 `armed` 时，空闲回收保留同一个 Agent 和 Session lease，允许宿主唯一 GoalRoundDriver 在会话保存完成后调度下一轮。目标终态或 disarm 后恢复普通空闲回收；owner 撤权、租约失效、scope 卸载和原有 `maxExecutionMs` 期限仍优先结束执行，不为每轮重置期限。若宿主缺少 GoalRoundDriver，armed 目标最多保持到该期限；此保留检查本身不派发任务。

当前 Web 只支持单已认证控制面的文本交互；图片、完整 queue/steer、跨渠道 owner 别名和子 Agent 历史仍未验收。原生 Web 安装、配置、Policy 与权限见 [Web owner 包说明](../assistant-web-owner/README.md)，浏览器探针及证据边界见 [E2E 指南](../../scripts/e2e/README.md)。

可选 Host 接口 `registerForegroundTaskObserver()` 只接受当前 Control Plane 拥有的注册，在认证 Inbox 的执行绑定后同步通知开始，资源释放并保存真实执行回执后通知结束。它不改变 schema 23 或学习来源摘要，不注册模型工具；注销、owner/服务换代及重启后不补发历史事件，观察错误不影响正常聊天。部署版本与 readiness 核对由 [Control Plane](../plugin-control-plane) 持有；这些事件本身不证明任务质量或具体工具调用。

Host 编排可从 `@dsh-enhanced/assistant-delivery/types` 读取无服务初始化的公开类型；该入口不激活 Delivery，也不授予消息发送权限。根构建先生成此类型入口，避免 Evaluation benchmark 与 Delivery 服务声明形成构建环。

## Host 回放的回复阻断

`blockAgentRepliesForReplay(agent, { operationId, maximumAttempts, expiresAt })` 为专用 Agent 建立永久拒绝回复的标记。真实 `reply()` 与完成偏好回合的回复在绑定、Policy、Outbox 前记录有界输入摘要并拒绝；其他 Agent 不受影响。关闭句柄、到期、限额和 Agent Fiber 卸载均不会恢复权限。它不授予发送权限、不新增模型工具，也不阻断其他 Host 发送 API；快照是进程内观察，不是外部签名或全局无副作用证明。见[回放组件契约](../../docs/effect-blocked-replay.md)。

## 持久主人提醒

`enqueueOwnerNotification` 为可信 Host 插件提供限定主人路由、原会话、身份版本和有效期的提醒入队接口。普通 `enqueueBackground` 不能写入保留的 `dsh.native-notice*` 元数据。投递前重查当前主人、绑定版本与代次、路由、期限及背景 `send` Policy；过期、撤权或换代的队列不会发送。

原生 Web 使用本地适配器将通知标为可读取，并由 `assistant-web-owner` 的会话输入区展示。通知不写入 Session 历史，不触发模型，也不表示目标已完成；`accepted` 不表示用户已阅读。读取沿用固定 Web 主人能力，并再次核验上述边界；不另设消息 `read` Policy。其他渠道复用已有适配器。对于已发生但结果未知的外部发送，重启恢复仅通过适配器查询结果；查到过去已发送不表示再次发送，也不会为重发授予权限。
