feat(feedback): add a /feedback command recorded through the command plane
Register a global `/feedback` command so a user can record a remark about the session without spending a model turn. `/feedback <text>` acknowledges; empty or whitespace-only input returns a usage error. The plugin appends no session event of its own. `dsh-commands` already writes a `command/run` / `command/done` pair for every dispatched command, carrying the verbatim text and the settled outcome, and both records are log-only and non-surface. The feedback is therefore durably in the session log and invisible to the model without this package touching the log format. Text is never parsed, so `/feedback /plan felt slow` records that literal content. Nothing consumes the records; capture is deliberately inert. New group `packages/feedback/` — no existing group owns feedback capture. Its row raises the packages/README.md word ceiling by 10, which had no headroom; one redundant sentence there was removed to offset most of the cost.
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-28-feedback-command.md
|
||||
2026-07-28-feedback-command.md: ae32d3908d568c4a511e8d9e2b8cf50569fb80bf
|
||||
2026-07-28-feedback-command.zh.md: f69dbf6a50161e7f5048b76be46bc4063f9e757a
|
||||
@@ -0,0 +1,61 @@
|
||||
# Agent Note: `/feedback` command
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-28-feedback-command.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
A user who notices something wrong mid-session has nowhere to put that observation. Telling the model wastes a turn, changes the conversation the user was having, and buries the remark in derived history where no later reader can find it. Writing it outside the session loses the context that makes it meaningful — which session, at which point, against which work.
|
||||
|
||||
The capture surface has to be usable at the moment of annoyance, which rules out anything requiring the user to leave the TUI, and it must not perturb the run in progress: no model tokens, no turn of work, no change to the request the user is waiting on.
|
||||
|
||||
## Decision
|
||||
|
||||
`@deepseek-ai/dsh-command-feedback` in `packages/feedback/command-feedback/` registers one global `feedback` command over `ctx.commands`. `/feedback <text>` acknowledges; bare or whitespace-only input returns a direct usage error. The handler is synchronous, injects only `commands`, and has no configuration.
|
||||
|
||||
The plugin appends **no session event of its own**. `dsh-commands` already writes a `command/run` / `command/done` pair for every dispatched command, carrying the command name, the verbatim unparsed suffix, the invocation source, and the settled outcome. Those records are log-only and non-surface, so the feedback lands in the session log and stays invisible to the model without this package contributing anything to the log format. The appends start persistence's ordinary eager drain; nothing forces a flush, so the acknowledgement reports that the entry is recorded in the log rather than already on disk.
|
||||
|
||||
Capture is deliberately inert: nothing in this repository reads those records back.
|
||||
|
||||
### Why no dedicated `session/feedback` event
|
||||
|
||||
An earlier iteration declared one. It was removed because it duplicated a record the registry already writes: both would carry the same text, appended microseconds apart, and a consumer would have to decide which is authoritative. Selecting `command/run` records by command name is enough to find feedback, and it keeps this package free of the session event format entirely — no `SessionEventMap` merge, no invariant relation, no persistence catalog entry.
|
||||
|
||||
The cost is that the recorded text is the raw suffix including its leading separator whitespace, and that feedback is distinguished from other commands only by name. Both are read-time concerns for a consumer that does not yet exist; neither justifies a second durable record now.
|
||||
|
||||
### Why the model never sees it
|
||||
|
||||
Feedback is about the session, not input to it. Injecting it as a user message would change the next model request, contradicting the requirement that recording not perturb the run, and would make the remark part of the conversation it comments on. `command/run` and `command/done` are absent from `SurfaceEventType`, so they cannot acquire a `surfaceOp` or enter derived history even by mistake.
|
||||
|
||||
### Verbatim text
|
||||
|
||||
Nothing is parsed. `/feedback /plan felt slow` records that literal text; the leading `/plan` is content, not a nested command. The handler trims only to decide whether any text was supplied. Control-word grammar of the kind `/goal` uses would make the corresponding literal feedback impossible to express, which is the opposite of what a capture surface is for.
|
||||
|
||||
### A new group
|
||||
|
||||
`packages/feedback/` is a new group because no existing one owns this. `goal/` is objective state, `session-title/` is titles, `core/` is the product spine. The group holds one package; a consumer would join it rather than forcing this one to grow.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Declare a dedicated `session/feedback` log-only event.** Implemented first, then removed. It gave feedback a first-class queryable type with pre-trimmed text, but duplicated the registry's record, added a `SessionEventMap` member and persistence-catalog entry to the frozen log format, and created two records of one act with no rule for which wins.
|
||||
|
||||
**Inject feedback as a user message via `agent.inject()`.** Needs no new event type and reuses the path `/goal` mutations take. Rejected: it makes the feedback model-visible, so it enters the next request, changes the run being commented on, and consumes tokens — contradicting all three parts of the no-perturbation requirement.
|
||||
|
||||
**Make `/feedback` a true no-op that records nothing.** The most literal reading of "does not do anything". Rejected because it makes the command pointless: the stated requirement was that the remark reach the session log.
|
||||
|
||||
**Register the command inside an existing package** such as `packages/ui/commands`. Avoids a new group and its README pair. Rejected: `ctx.commands` is the registry, not a home for arbitrary command implementations, and the requester asked for a standalone package.
|
||||
|
||||
**Parse structure out of the text** (category prefixes, severity markers). Rejected as speculative: no consumer exists to use the structure, and any control-word grammar makes the corresponding literal feedback unrecordable. Verbatim text is the widest surface a future consumer can narrow; a parsed one cannot be widened after the fact.
|
||||
|
||||
**Add a model-facing tool instead of a slash command.** Rejected: feedback is a direct human observation. Routing it through the model spends a turn, lets the model paraphrase the user's words, and makes the record contingent on the model choosing to call the tool.
|
||||
|
||||
## Consequences
|
||||
|
||||
The TUI mounts the command unconditionally — no configuration, no dependency on the goal stack. The headless CLI, ACP, and JSON-RPC apps do not consume `ctx.commands`, so `/feedback` is unavailable there.
|
||||
|
||||
This package is now small enough that its whole contract is the command definition plus one validation branch. It owns no session event, so it needs no invariant relation and cannot affect replay, forking, or crash recovery.
|
||||
|
||||
Deferred: no consumer; no structured fields; no amend or withdraw, since the log is append-only and this package adds no tombstone; the recorded text is untrimmed, so a consumer trims at read time; and no explicit durability barrier, so an entry recorded immediately before a crash can be lost with any other unflushed tail.
|
||||
|
||||
No snapshot accompanies this change. AGENTS.md asks for a keyless snapshot through a runnable example for product-user-visible behavior; this was skipped at the requester's explicit direction. The package tests plus a real Loader composition test over a `cordis.yml` are the whole of the evidence, alongside interactive verification in the assembled TUI.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Agent Note: `/feedback` 命令
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-28-feedback-command.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
用户在会话中途发现问题时,没有地方记下这个观察。告诉模型会浪费一个轮次、改变用户原本进行的对话,并把这条评论埋进派生历史,使后续读者无法找到它。写到会话之外则会丢失让它有意义的上下文:属于哪个会话、处于哪个时点、针对哪项工作。
|
||||
|
||||
采集接口必须能在用户产生不满的那一刻使用,因此任何需要用户离开 TUI 的方案都不可行;它还不能扰动正在进行的运行:不消耗模型 token、不产生工作轮次、不改变用户正在等待的请求。
|
||||
|
||||
## 决策
|
||||
|
||||
位于 `packages/feedback/command-feedback/` 的 `@deepseek-ai/dsh-command-feedback` 通过 `ctx.commands` 注册一个全局 `feedback` 命令。`/feedback <text>` 给出确认;空输入或仅含空白的输入返回直接用法错误。处理器是同步的,只注入 `commands`,且没有任何配置。
|
||||
|
||||
该插件**不追加属于自己的会话事件**。`dsh-commands` 已经为每个已分发命令写入一对 `command/run` / `command/done`,携带命令名、原样未解析的后缀、调用来源以及结算结果。这些记录仅写入日志且非 surface,因此反馈会进入会话日志并对模型保持不可见,而本包无需向日志格式贡献任何内容。这些追加会启动持久化的常规即时排空;没有任何环节强制 flush,因此确认文本报告的是条目已记录在日志中,而非已经落盘。
|
||||
|
||||
采集刻意不产生后续动作:本仓库中没有任何代码读回这些记录。
|
||||
|
||||
### 为何不设专用的 `session/feedback` 事件
|
||||
|
||||
早先的实现声明过该事件,后来将其移除,因为它重复了注册表已经写入的记录:两者会携带相同文本、相隔极短时间先后追加,而消费方还得判断以哪一条为准。依据命令名筛选 `command/run` 记录已足以找到反馈,同时让本包完全不涉及会话事件格式——没有 `SessionEventMap` 合并、没有不变式关系、没有持久化目录条目。
|
||||
|
||||
代价是被记录的文本为原始后缀,包含其前导分隔空白;且反馈仅凭命令名与其他命令相区分。两者都属于尚不存在的消费方在读取时需要处理的问题,目前都不足以支撑再增加一条持久记录。
|
||||
|
||||
### 为何模型永不看到它
|
||||
|
||||
反馈是关于会话的,而不是会话的输入。将其作为 user 消息注入会改变下一次模型请求,与「记录不得扰动运行」的要求相冲突,也会让该评论成为它所评论的那段对话的一部分。`command/run` 与 `command/done` 不属于 `SurfaceEventType`,因此即便出错也无法获得 `surfaceOp` 或进入派生历史。
|
||||
|
||||
### 原样文本
|
||||
|
||||
不做任何解析。`/feedback /plan felt slow` 记录的就是该字面文本;开头的 `/plan` 是内容,而非嵌套命令。处理器仅为判断是否提供了文本而修剪。若采用 `/goal` 那样的控制词语法,对应的字面反馈将无法表达,这与采集接口的目的正好相反。
|
||||
|
||||
### 一个新的分组
|
||||
|
||||
`packages/feedback/` 是新分组,因为现有分组都不拥有此职责:`goal/` 负责目标状态,`session-title/` 负责标题,`core/` 是产品主干。该分组目前只有一个包;未来的消费方应加入该分组,而不是迫使这个包不断膨胀。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**声明专用的 `session/feedback` 仅日志事件。** 先实现后移除。它让反馈拥有一等的可查询类型和预先修剪的文本,但重复了注册表的记录,向已冻结的日志格式新增了一个 `SessionEventMap` 成员与持久化目录条目,并使同一行为产生两条记录而没有取舍规则。
|
||||
|
||||
**通过 `agent.inject()` 将反馈作为 user 消息注入。** 无需新增事件类型,并复用 `/goal` 变更所走的路径。已否决:它会让反馈对模型可见,从而进入下一次请求、改变正被评论的那次运行并消耗 token——与「不得扰动」要求的三个方面全部冲突。
|
||||
|
||||
**让 `/feedback` 成为真正的空操作,什么都不记录。** 这是对「什么都不做」最字面的理解。已否决:这会使命令失去意义——明确的要求是让这条评论进入会话日志。
|
||||
|
||||
**在现有包中注册该命令**,例如 `packages/ui/commands`。可省去新分组及其双语 README。已否决:`ctx.commands` 是注册表,而不是任意命令实现的归属地;且请求者明确要求独立的包。
|
||||
|
||||
**从文本中解析结构**(类别前缀、严重程度标记)。已否决,属于投机设计:目前没有消费方使用该结构,而任何控制词语法都会让对应的字面反馈无法记录。原样文本是未来消费方可以收窄的最宽接口;而已被解析的接口无法事后放宽。
|
||||
|
||||
**改为提供面向模型的工具。** 已否决:反馈是人类的直接观察。经由模型会消耗一个轮次、让模型改写用户的原话,并使记录取决于模型是否选择调用该工具。
|
||||
|
||||
## 后果
|
||||
|
||||
TUI 无条件挂载该命令:没有配置,也不依赖 goal 栈。无头 CLI、ACP 和 JSON-RPC 应用不消费 `ctx.commands`,因此 `/feedback` 在那里不可用。
|
||||
|
||||
本包现已小到其全部契约就是命令定义加一个校验分支。它不拥有任何会话事件,因此无需不变式关系,也不可能影响回放、fork 或崩溃恢复。
|
||||
|
||||
延期事项:没有消费方;没有结构化字段;不支持修改或撤回,因为日志仅追加且本包不新增 tombstone;被记录的文本未修剪,需由消费方在读取时处理;且没有显式持久化屏障,因此紧临崩溃前记录的条目可能与其他未 flush 的尾部一同丢失。
|
||||
|
||||
本次变更不附带 snapshot。AGENTS.md 要求面向产品用户的可见行为变更通过可运行示例附带无密钥 snapshot;此项按请求者的明确指示跳过。包测试连同一个基于真实 `cordis.yml` 的 Loader 组合测试即为全部证据,此外还有在组装后 TUI 中的交互验证。
|
||||
Reference in New Issue
Block a user