反馈
一句话版:反馈族有两个刻意分离的契约——
/feedback命令在会话日志里追加一条只读的feedback/record事件(log-only,不进模型),而消息反馈(ctx.messageFeedback)是绑在单条 assistant 消息上的可编辑评分/备注侧车,存 storage 域、可list/put/delete。两者都不进入模型对话。
反馈让"人"把意见留给系统,但不污染模型上下文:命令反馈是会话日志里不可变的一条备注,消息反馈是宿主持有的、可改可删的逐消息评分。
一、两个契约
| 包 | 角色 | ctx key |
|---|---|---|
command-feedback | 与触发方式无关的 feedback/record 事件 + 面向人的 /feedback 生产者 | — |
message-feedback | 绑定生命周期的逐消息评分/备注侧车 + Host messageFeedback.list/put/delete Remote 契约 | messageFeedback |
命令反馈是 log-only:永不进模型上下文或派生历史。挂载 dsh-session-telemetry-otel 时,它观察 feedback/record 来释放待发的遥测前缀,或警告"遥测关闭时反馈只留在本地";捕获本身与这一策略无关。
消息反馈不是 Session 事件或投影:它待在 storage 域的侧车里,不触发遥测交接。Host Remote 契约随服务发布,客户端 Remote 聚合与 UI 消费者另行持有。
二、命令反馈(command-feedback)
包导出 recordFeedback(session, text),追加一条 log-only 的 feedback/record 事件。它的插件经 ctx.commands 注册一个全局命令,因此每个组合的命令适配器都能发现它;发布的 Web 客户端无需一个模型轮次即可执行。
它做什么、不做什么:
recordFeedback是与命令无关的写入路径:拒绝空(归一化后)文本,追加feedback/record { text }。别的 UI、hook 或宿主集成可以不构造斜杠命令直接调用它/feedback处理器用这个生产者,且不启动任何模型工作- 可选的
dsh-session-telemetry-otel消费者观察该事件,不改动其捕获契约 - 反馈文本只出现在唯一一份持久载荷
feedback/record里。dsh-commands仍追加通用的command/run/command/done事件对,但此定义设recordInput: false,command/run省略args,command/done只带结果。三个事件都是 log-only,不出现在有序 surface、deriveMessages()或模型请求里 - 追加会启动持久化的常规 eager drain,但生产者不强制
session/flush:所以确认只意味着反馈进了日志,不代表已落盘 - 事件是权威来源而非命令记录:反馈可能经
/feedback之外的触发到达;把载荷留在command/run之外,避免两份记录带同一段文本
三、/feedback 命令契约
| 输入 | 结果 |
|---|---|
/feedback <text> | 追加 feedback/record,确认信息带 Feedback recorded for session {sessionId}、User: {userId},以及会话共享披露 |
/feedback | 直接返回用法错误;纯空白输入视作空 |
- 首尾空白被丢弃,除此之外不解析:无截断、大小写折叠或控制词
- 看起来像别的命令的文本(如
/feedback /plan felt slow)就是反馈内容 - 重复命令各自产生自己的事件;不替换、不合并
四、会话共享披露
确认信息会点名接收会话 id,并报告该会话如何共享,来自挂载的 telemetry 服务(经插件上下文 ctx.get('telemetry'),不是声明的注入)。披露是后端 TelemetrySharingStatus 决定的一句话:
| 披露状态 | 确认句子 |
|---|---|
full | Session sharing is enabled. |
feedback-only | Session sharing is feedback-gated; recording feedback releases the session prefix for sharing. |
disabled | Session sharing is disabled. |
| 无服务 | Session sharing is not configured. |
披露只陈述部署当前的共享策略,从不承诺送达或保留。full/feedback-only 下,记录交给后端的非阻塞入队,SDK 负责批处理、重试与丢失策略,所以句子不声称任何内容已到达收集器。披露不追加事件,也不进模型 surface。
五、消息反馈侧车(message-feedback)
宿主持有的、针对一条已定稿 assistant 消息的可编辑反馈。插件注册 ctx.messageFeedback,在 storage 域为每个 Session 持久化一条绑定生命周期的侧车行,并发布 Host 的 messageFeedback.list / messageFeedback.put / messageFeedback.delete 一元 Remote 契约。它独立于不可变的 Session 级 feedback/record 事件,不产生遥测交接。
配置:
| key | 含义 |
|---|---|
maxNoteBytes | 必填的正安全整数:单条可选备注的最大 UTF-8 字节长度 |
备注必须含至少一个非空白字符,但接受的文本原样存储、不 trim。省略 note 表示"期望值没有备注",所以版本匹配的实质性 put 会清掉已有备注。备注校验先于 Session 查找,因此对缺失的 Session 也可能返回 note-blank / note-too-large,而不会碰持久化。
- id: message-feedback
name: '@deepseek-ai/dsh-message-feedback'
config:
maxNoteBytes: 8192
服务注入 storageDomain、sessionPersistence、sessions。持久域 message_feedback,每个 SessionId 一行 sessions 表记录。
六、数据、生命周期与持久性
MessageFeedbackItem 含 messageId、rating: 'positive' | 'negative'、可选 note、一个只用于等值比较的不透明 version,以及宿主分配的 createdAt/updatedAt Unix 毫秒时间戳。实质性更新保留 createdAt、替换 version、并保证 updatedAt 不倒退。list 返回按首次创建顺序的新鲜不可变快照;更新某项保留其位置,删除后再重建则追加为新项。
每行携带被检视的 Session 头部身份 {createdAt, cwd}。不匹配视作缺失:list 返回空 items,delete 返回缺失后置条件,put 可能用绑定当前身份的替换掉陈旧行。这挡住了 SessionId 被复用时头部身份不同的情况;fork 用不同的 Session 身份,不继承反馈行。
put 只接受非空、append 来源的 assistant/message;替换来源的消息、空的 usage-only assistant 记录、非 assistant 记录都返回 target-not-found。
初验后,put 在写侧车前建立持久性屏障:匹配的活 Session 经规范的 ctx.sessions.flush 检查点提交,然后活路径与冷路径都从 sequence zero 经 SessionPersistence.readFrom 物理读。缺失 flush 参与者、身份变化、目标消失或物理读失败都会阻止侧车提交——所以持久的反馈绝不会先于持久的目标消息。
七、Compare-and-set 与幂等
ifVersion: null只请求创建;对已存在项的每个请求都要求其精确当前版本,包括"期望值已相同"的 no-op- 检查按消息而非按 Session:改一项不会与另一项冲突
- 每次实质性创建/更新分配一个新的不透明 UUID token,防止陈旧写跨越 ABA 值循环
- 版本匹配的 no-op 返回已存项,版本与时间戳不变;丢失成功响应后,用旧 token 重试会收到
version-conflict.current,调用方无需额外读即可比对权威项 delete在项已缺失时忽略ifVersion,成功后始终返回稳定的{ absent: true }- 每个 Session 的 promise 队列把检视、持久性校验、侧车读、比较、整行写串行化;storage 域本身没有跨进程条件写
- 插件 dispose 关闭变异准入、排干每个 Session 队列里已接收的操作,然后才关闭 storage 域;dispose 开始后提交的变异作为生命周期失败拒绝
八、Service / Host Remote 契约
GatewayService 与 @Remote 发布同一组 MessageFeedbackService 方法,Host 端点名 messageFeedback.list / messageFeedback.put / messageFeedback.delete。每个方法返回判别业务联合:{ ok: true, value } 或 { ok: false, error };操作级存储、损坏或缺失持久性监听器的失败会拒绝,而不是被误标为业务错误。
| 方法 | 请求 | 成功 value | 拒绝 error.code |
|---|---|---|---|
list | { sessionId } | { items } | session-not-found |
put | { sessionId, messageId, rating, note?, ifVersion } | 已提交的 MessageFeedbackItem | session-not-found、target-not-found、version-conflict、note-blank、note-too-large |
delete | { sessionId, messageId, ifVersion } | { absent: true } | session-not-found、version-conflict |
MessageFeedbackVersionConflict 返回权威的 current 项(无项时为 null),让调用方无需第二次 list 即可协调当前 rating/note/version。MessageFeedbackNoteTooLarge 同时返回 maxBytes 与 actualBytes。客户端 Remote 聚合暂未挂载生成的客户端贡献;宿主调用方无需该客户端装配即可使用 service/Remote 契约。
九、挂载状态
| 包 | 默认挂载 | 默认配置 |
|---|---|---|
command-feedback | base 默认挂载(无条件,无配置) | 无 |
message-feedback | web-app 默认挂载 | maxNoteBytes: 8192 |
/feedback 只在 Web 客户端暴露(经命令适配器);headless、ACP 自动化、JSON-RPC 不提供命令适配器,故不可用。消息反馈的 Host Remote 契约已随服务发布,但客户端 Remote 聚合与 UI 消费者另行持有、暂缺。
十、对模型的可见性
命令反馈:模型什么都看不到——斜杠输入、feedback/record、确认信息都不进模型请求(都无 surfaceOp,不进有序 surface / deriveMessages() / 系统提示)。录制反馈不改变该轮剩余请求。Token 效应为零;KV cache 独立,不影响前缀复用。
消息反馈:ctx.messageFeedback 不注册工具、提示段、模型上下文或 Session 事件;反馈待在宿主侧车,除非另有消费者显式暴露。Token 效应为零;KV cache 独立。
十一、已知限制
- 命令反馈:无检索/管理 surface、无结构化字段(一条自由文本)、无 amend/withdraw(日志 append-only)、无持久化屏障(确认≠落盘)、全新会话上无可见确认行、只在 Web 暴露
- 消息反馈:客户端聚合与 UI 暂缺;CAS 是单进程的(多宿主进程仍可能丢更新);无持久 Session 删除级联;detach/catalog 收编窗口内可能收到
session-not-found;头部身份不是内容指纹;无鉴权调用者边界;item 数量与单 Session 行字节总量未设上限
验证
# 命令反馈已默认挂载(base)
dsh web --dump-config | grep -iE "command-feedback"
# 消息反馈已默认挂载(web-app)
dsh web --dump-config | grep -iE "message-feedback"
# 会话日志里看 feedback/record 事件
zstdcat ~/.dsh/sessions/*/*/session.jsonl.zstd | grep -E '"feedback/record"' | tail
下一步
- 自定义命令与用户交互:命令注册表与 command/run 事件对
- 事件系统:feedback/record 在会话日志词汇里的位置
- 存储层:消息反馈侧车存哪(storage 域)