@deepseek-ai/dsh-message-feedback 0.1.1-rc.2 → 0.1.2-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/feedback/message-feedback/README.md
5
- README.md: b85104abb983eb3b33ab6ddad9328e6f5f3b1fc9
6
- README.zh.md: be4cffb4d3081efe0b52d66cee42089f21d4280f
5
+ README.md: dc55231d7af3af57fa116d52ddb7ec58ab0a153e
6
+ README.zh.md: 4a91880a42a7de6042a8a9be056641f2c07ebb8a
package/README.md CHANGED
@@ -1,18 +1,37 @@
1
+ ---
2
+ description: "Per-message ratings and notes for finalized assistant messages, for users and maintainers choosing, composing, or debugging the feedback service."
3
+ kind: "package-reference"
4
+ ---
5
+
1
6
  # @deepseek-ai/dsh-message-feedback
2
7
 
3
8
  English | [中文](README.zh.md)
4
9
 
5
- Host-owned editable feedback for one finalized assistant message. The package registers `ctx.messageFeedback`, persists one lifecycle-bound sidecar row per Session in storage-domain, and publishes the Host `messageFeedback.list`, `messageFeedback.put`, and `messageFeedback.delete` unary Remote contract. It is separate from the immutable Session-level `feedback/record` event and performs no telemetry handoff. The [message-feedback sidecar Agent Note](../../../.agents/notes/implemented/architecture/2026-08-10-message-feedback-sidecar.md) owns the design boundary.
10
+ ## Summary
6
11
 
7
- Public request, value, version, and failure types are exported from the package root and `@deepseek-ai/dsh-message-feedback/types`; [`src/types.ts`](src/types.ts) is their source.
12
+ `dsh-message-feedback` lets product surfaces offer per-message feedback: a user marks an assistant message positive or negative and can attach a short note, and the rating stays with that message. Ratings are stored with the session, survive restarts, and never enter model history or telemetry. Product surfaces read, create, and change ratings through the `messageFeedback` service, whose `list`, `put`, and `delete` operations are the whole surface. The one deployment setting is the maximum note length (`maxNoteBytes`), which the Web bundle sets to 8192. Browser controls live in a separate client package; this package provides the service itself.
8
13
 
9
- ## Configuration
14
+ ## Table of Contents
10
15
 
11
- | key | meaning |
12
- |---|---|
13
- | `maxNoteBytes` | Required positive safe integer: maximum UTF-8 byte length of one optional note. |
16
+ - [Use this package](#use-this-package)
17
+ - [Understand the implementation](#understand-the-implementation)
18
+ - [Further Exploration](#further-exploration)
19
+ - [Model Experience](#model-experience)
20
+ - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
21
+ - [Dev Note](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## Use this package
14
27
 
15
- Notes must contain at least one non-whitespace character, but accepted text is stored verbatim rather than trimmed. Omitting `note` means the desired value has no note, so a version-matched material `put` clears an existing note. Note validation precedes Session lookup and can therefore return `note-blank` or `note-too-large` for a missing Session without touching persistence.
28
+ Choose this service when a product surface should let users rate and annotate individual assistant messages. Feedback attaches only to a finalized message — one that has already been sent and using the service never starts or resumes an agent. A custom app mounts the service together with session persistence and storage; the shipped Web bundle already composes all of it with `maxNoteBytes: 8192`.
29
+
30
+ ### Configuration
31
+
32
+ | Field | Default | Meaning |
33
+ |---|---|---|
34
+ | `maxNoteBytes` | required | Maximum UTF-8 byte length accepted for one optional note. |
16
35
 
17
36
  ```yaml
18
37
  - id: message-feedback
@@ -21,42 +40,83 @@ Notes must contain at least one non-whitespace character, but accepted text is s
21
40
  maxNoteBytes: 8192
22
41
  ```
23
42
 
24
- The service injects `storageDomain`, `sessionPersistence`, and `sessions`. Its durable domain is `message_feedback`, with one `sessions` table row per `SessionId`.
43
+ A note must contain at least one non-whitespace character and fit within the configured byte length; a blank note is rejected with `note-blank` and an oversized one with `note-too-large`. Accepted text is stored exactly as submitted — nothing is trimmed. The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-message-feedback) is the exhaustive source for every accepted field and its JSDoc.
25
44
 
26
- ## Data, lifecycle, and durability
45
+ ### Reading and changing feedback
27
46
 
28
- `MessageFeedbackItem` contains `messageId`, `rating: 'positive' | 'negative'`, optional `note`, an opaque equality-only `version`, and Host-assigned `createdAt`/`updatedAt` Unix-millisecond timestamps. A material update preserves `createdAt`, replaces `version`, and keeps `updatedAt` from moving backward. `list` returns fresh immutable snapshots in first-creation order; updating an item retains its place, while deleting and later recreating it appends a new item.
47
+ Callers use three operations to read and change feedback for a session:
29
48
 
30
- Each stored row carries the inspected Session header identity `{createdAt, cwd}`. A mismatch is treated as absence: `list` returns an empty `items` array, `delete` returns the absent postcondition, and `put` may replace the stale row with one bound to the current identity. This fences a reused `SessionId` when its header identity differs. Forks use a distinct Session identity and receive no feedback-row copy.
49
+ | Operation | Request | Success | Rejected when |
50
+ |---|---|---|---|
51
+ | `list` | the session id | the current ratings and notes, in creation order | the session is not found |
52
+ | `put` | session, message, rating, optional note, expected version | the stored rating and note | session not found, message is not a valid target, version conflict, blank or oversized note |
53
+ | `delete` | session, message, expected version | the rating is absent | session not found, version conflict |
31
54
 
32
- `SessionPersistence.inspect()` supplies a cold-safe observation without publishing or resuming an Agent and without committing cold repair. For a Session without a live owner, `listSnapshots()` first decides definite absence; an `inspect()` failure for a catalogued Session remains an infrastructure failure rather than being guessed into `session-not-found`. `put` accepts only a non-empty, append-origin `assistant/message` with the requested `MessageId`; replacement-origin messages, empty usage-only assistant records, and non-assistant records return `target-not-found`.
55
+ Every change must be based on the version the service returned for that rating: a change based on an older version is rejected with `version-conflict`, and the reply carries the current rating so the caller can see what changed without another read. Deleting a rating that is already absent succeeds, and concurrent changes to different messages do not conflict. An omitted note clears an existing note.
33
56
 
34
- After initial validation, `put` establishes a durability barrier before writing the sidecar. A matching live Session commits through the canonical `ctx.sessions.flush` checkpoint, then both live and cold paths are physically read from sequence zero through `SessionPersistence.readFrom`. The resulting observation's header identity and target are validated again. A missing flush participant, changed identity, vanished target, or physical-read failure prevents the sidecar commit, so durable feedback never precedes the durable target message.
57
+ ### What you can rate
35
58
 
36
- Message feedback is not Session-log content or a Session projection. It emits no `feedback/record` event, does not enter model history, and does not trigger `FEEDBACK_ONLY` telemetry release.
59
+ A rating attaches to one finalized assistant message: the message must exist and be an assistant message that was sent. User messages, empty assistant placeholders, and replaced messages are not valid targets and are rejected with `target-not-found`. Once recorded, the rating and note stay with that message and survive restarts; a fork of the session starts with no feedback.
37
60
 
38
- ## Service and Host Remote contract
61
+ ### Durability
39
62
 
40
- The same three `MessageFeedbackService` methods are published by `TypertRemoteService` and `@Remote`; the Host endpoint names are `messageFeedback.list`, `messageFeedback.put`, and `messageFeedback.delete`. Every method returns a discriminated business union: `{ ok: true, value }` or `{ ok: false, error }`. Operational storage, corruption, or missing-durability-listener failures reject instead of being mislabeled as business errors.
63
+ A rating is committed only after the message it refers to is durably stored, so feedback never points at a message that can be lost. Reading or writing feedback never starts or resumes an agent; the service inspects the persisted session directly.
41
64
 
42
- | Method | Request | Success `value` | Rejected `error.code` |
43
- |---|---|---|---|
44
- | `list` | `MessageFeedbackListRequest { sessionId }` | `MessageFeedbackListValue { items }` | `session-not-found` |
45
- | `put` | `MessageFeedbackPutRequest { sessionId, messageId, rating, note?, ifVersion }` | committed `MessageFeedbackItem` | `session-not-found`, `target-not-found`, `version-conflict`, `note-blank`, `note-too-large` |
46
- | `delete` | `MessageFeedbackDeleteRequest { sessionId, messageId, ifVersion }` | `MessageFeedbackDeleteValue { absent: true }` | `session-not-found`, `version-conflict` |
65
+ -----
47
66
 
48
- `MessageFeedbackVersionConflict` returns the authoritative `current` item, or `null` when no item exists. This lets a caller reconcile the current rating, note, and version without a second `list` request. `MessageFeedbackNoteTooLarge` returns both `maxBytes` and `actualBytes`. The Client Remote aggregate does not mount the generated client contribution yet; Host callers can use the service/Remote contract without that client assembly.
67
+ <a id="understand-the-implementation"></a>
68
+ ## Understand the implementation
49
69
 
50
- ## Compare-and-set and idempotency
70
+ <details>
71
+ <summary>Implementation internals — click to expand</summary>
51
72
 
52
- `ifVersion: null` requests creation only; every request for an existing item requires its exact current version, including a no-op whose desired value already matches. The check is per message rather than per Session, so changing one item does not conflict with another. Every material create or update assigns a fresh opaque UUID token, preventing stale writes from crossing an ABA value cycle.
73
+ ### Design concept
53
74
 
54
- A matching-version no-op returns the already stored item with unchanged version and timestamps. After a lost success response, a retry with the old token receives `version-conflict.current`; the caller can compare that authoritative item with its desired value without an extra read. `delete` ignores `ifVersion` when the item is already absent and always returns the stable `{ absent: true }` postcondition after success.
75
+ The service keeps feedback outside the session log entirely: each session owns one sidecar row in a storage domain, so a rating can never be confused with conversation content, model history, or telemetry. The sidecar is only ever committed after the message it references is durable the row extends the target log instead of preceding it. Every operation returns a business result that distinguishes a handled failure (missing session, invalid target, stale version, bad note) from an infrastructure failure, which rejects instead of being mislabeled.
55
76
 
56
- A per-Session promise queue encloses inspection, durability validation, sidecar read, comparison, and whole-row write. These semantics serialize concurrent mutations through one service instance; storage-domain itself has no cross-process conditional write.
77
+ ### What a sidecar holds
57
78
 
58
- Plugin disposal closes mutation admission, drains every operation already accepted into the per-Session queues, and only then closes the storage domain. A mutation submitted after disposal begins rejects as a lifecycle failure instead of entering a closing domain.
79
+ One row per session binds the inspected session identity (`createdAt`, `cwd`) to its feedback items; the identity fences a reused session id, so a row from an earlier lifecycle is invisible and a fork starts with no feedback. Items are immutable values a change writes a new version of the item, preserving its creation time — and the row schema rejects duplicate message ids and reused versions so lookup stays unambiguous. The exact row schema and validation live in [`src/spec.ts`](src/spec.ts).
59
80
 
81
+ ### Concurrency
82
+
83
+ Mutations are optimistic and per message: a caller sends the version it last observed, a stale version is rejected with the authoritative current item so the caller reconciles without another read, and every material change mints a fresh version token so a stale write can never masquerade as current. A per-session queue serializes the whole read-compare-write through one service instance; storage provides no cross-process conditional write, which is the Known Limitation below.
84
+
85
+ ### Durability and target validation
86
+
87
+ A write is staged, verified, then committed: the target message is flushed through the canonical checkpoint, the physical log prefix is re-read, and only then is the sidecar row written — feedback can never reference a message that is not durable. Cold sessions are inspected without resuming an agent, absence is decided from the persistence catalog rather than guessed, and only a real, sent assistant message is a valid target. The flush and inspect path lives in [`src/index.ts`](src/index.ts).
88
+
89
+ ### Failure modes
90
+
91
+ The service fails closed: disposal drains in-flight writes before closing the domain, a write submitted after disposal starts is rejected as a lifecycle failure, and invalid configuration or a read before domain initialization fails loudly.
92
+
93
+ ### Source map
94
+
95
+ | File | Role |
96
+ |---|---|
97
+ | [`src/index.ts`](src/index.ts) | Service class: config validation, per-Session queue, durability barrier, `@Remote` methods |
98
+ | [`src/types.ts`](src/types.ts) | Public request, value, and failure vocabulary (types only, for generated Remote clients) |
99
+ | [`src/spec.ts`](src/spec.ts) | Storage-domain declaration: `message_feedback` domain, `sessions` table, row schemas |
100
+ | [`src/invariant.ts`](src/invariant.ts) | Invariant companion (no runtime invariant; the domain schema validates rows on reopen) |
101
+
102
+ </details>
103
+
104
+ -----
105
+
106
+ <a id="further-exploration"></a>
107
+ ## Further Exploration
108
+
109
+ Read these pages when the package-level contract is not enough. They move from the subsystem types and design boundary to the persistence primitives and the browser consumer that drives this service.
110
+
111
+ - [Feedback subsystem](../../../docs/subsystems/feedback.md) — the public types, Remote contract, and Web consumer details.
112
+ - [Message-feedback sidecar decision](../../../.agents/notes/implemented/architecture/2026-08-10-message-feedback-sidecar.md) — the design boundary that keeps this sidecar out of Session-log content.
113
+ - [Session persistence subsystem](../../../docs/subsystems/persistence.md) — `inspect`, `readFrom`, and `flush` semantics behind the durability barrier.
114
+ - [dsh-client-ui-message-feedback](../../client/ui-message-feedback/README.md) — the browser consumer that drives the Host Remote contract.
115
+ - [Feedback package map](../README.md) — where per-message feedback sits next to the log-only capture command.
116
+
117
+ -----
118
+
119
+ <a id="model-experience"></a>
60
120
  ## Model Experience
61
121
 
62
122
  ### Local message-feedback state
@@ -75,10 +135,28 @@ Independent. Listing or mutating message feedback does not touch a model request
75
135
 
76
136
  ## Known Limitations and Deferred Work
77
137
 
78
- - **Client aggregate and UI are absent** — the Host Remote contract ships, but the Client Remote aggregate contribution and any UI consumer are separately owned and deferred.
79
- - **Compare-and-set is single-process** — the per-Session queue serializes one service instance only; multiple Host processes writing one storage root can still lose updates because storage-domain exposes no cross-process conditional write.
80
- - **No durable Session deletion cascade** — Session persistence has no deletion API, and `session/disposed`/`host/session-removed` mean detach rather than durable deletion. The service therefore retains empty rows and may leave orphan rows after out-of-band log removal instead of deleting valid feedback on detach.
138
+ <a id="known-limitations-and-deferred-work"></a>
139
+
140
+
141
+ These limits define when the service is a poor fit or needs special operational care. They are current package constraints, not a task backlog.
142
+
143
+ - **Compare-and-set is single-process** — the per-Session queue serializes one service instance only; storage-domain has no cross-process conditional write, so multiple Host processes writing one storage root can still lose updates.
144
+ - **No durable Session deletion cascade** — Session persistence has no deletion API, and `session/disposed`/`api-session/removed` mean detach rather than durable deletion. The service therefore retains empty rows and may leave orphan rows after out-of-band log removal instead of deleting valid feedback on detach.
81
145
  - **Detach/catalog retirement window** — a request in the narrow interval after live detach but before the persistence catalog materializes the header can receive `session-not-found`; callers retry after retirement materialization.
82
146
  - **Header identity is not a content fingerprint** — `{createdAt, cwd}` detects reuse only when those fields differ; a cloned log retaining the same header identity is indistinguishable.
83
147
  - **Trusted caller boundary** — `list`/`put`/`delete` carry no authenticated actor or audit identity. A deployment must expose the Host gateway only through its trusted or separately authenticated boundary until authorization and attribution are added.
84
148
  - **Catalog and row bounds** — a cold request scans the complete Session snapshot catalog because persistence has no lookup-by-id metadata operation. `maxNoteBytes` bounds one note, but the item count and aggregate retained bytes of one Session row are not capped; an indexed metadata read and deployment-owned row bound remain deferred until a concrete consumer defines their policy.
149
+
150
+ <a id="dev-note"></a>
151
+ ### Dev Note
152
+
153
+ <details>
154
+ <summary>Working context for maintainers — click to expand</summary>
155
+
156
+ This Dev Note is working context for maintainers; it is explicitly non-authoritative. Shipped behavior, limits, and rationale live in the sections above, the package code, and the linked Agent Note.
157
+
158
+ - The browser controls and the client Remote mount live in `dsh-client-ui-message-feedback` and `dsh-api-remotes`; their open items belong to those packages' notes.
159
+ - The trusted-caller limitation is the open authorization direction: the Host gateway records no actor or audit identity, and any authentication layer must land at the deployment boundary before the service exposes per-user attribution.
160
+ - Note validation precedes Session lookup by design, so `note-blank` and `note-too-large` win over `session-not-found` for a missing Session; tests pin this order.
161
+
162
+ </details>
package/README.zh.md CHANGED
@@ -1,18 +1,37 @@
1
+ ---
2
+ description: "针对已完成 assistant 消息的逐消息评分与备注,供用户与维护者选择、组合或排查该反馈服务。"
3
+ kind: "package-reference"
4
+ ---
5
+
1
6
  # @deepseek-ai/dsh-message-feedback
2
7
 
3
8
  [English](README.md) | 中文
4
9
 
5
- 本包提供由 Host 拥有、针对单条已完成 assistant 消息的可编辑反馈。它注册 `ctx.messageFeedback`,在 storage-domain 中为每个 Session 持久化一条绑定生命周期的伴随记录(sidecar),并发布 Host `messageFeedback.list`、`messageFeedback.put` 与 `messageFeedback.delete` 一元 Remote 契约。它与不可变的 Session 级 `feedback/record` 事件相互独立,不执行遥测交接。[消息反馈伴随记录 Agent Note](../../../.agents/notes/implemented/architecture/2026-08-10-message-feedback-sidecar.zh.md)拥有其设计边界。
10
+ ## 概述
6
11
 
7
- 公开的请求、值、版本与失败类型从包根入口及 `@deepseek-ai/dsh-message-feedback/types` 导出;其源码为 [`src/types.ts`](src/types.ts)。
12
+ `dsh-message-feedback` 让产品界面提供逐消息反馈:用户可以把一条 assistant 消息标记为好评或差评,并可附上简短备注,评分会与该消息绑定。评分与会话一起保存,重启后依然存在,并且绝不会进入模型历史或遥测。产品界面通过 `messageFeedback` 服务读取、创建和修改评分,其 `list`、`put`、`delete` 三个操作就是全部对外表面。唯一需要部署方设置的项是备注最大长度(`maxNoteBytes`),Web 组合将其设为 8192。浏览器控件位于独立的客户端包中;本包提供服务本身。
8
13
 
9
- ## 配置
14
+ ## 目录
10
15
 
11
- | 键 | 含义 |
12
- |---|---|
13
- | `maxNoteBytes` | 必填正 safe integer:一条可选备注的最大 UTF-8 字节长度。 |
16
+ - [使用本包](#use-this-package)
17
+ - [理解实现](#understand-the-implementation)
18
+ - [进一步探索](#further-exploration)
19
+ - [模型体验](#model-experience)
20
+ - [已知限制与延期工作](#known-limitations-and-deferred-work)
21
+ - [开发备注](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## 使用本包
14
27
 
15
- 备注必须包含至少一个非空白字符,但通过校验的文本按原样存储,不会 trim。省略 `note` 表示目标值不含备注,因此 version 匹配的实质 `put` 会清除已有备注。备注校验早于 Session 查找,因此即使 Session 不存在,也可能在不访问持久化的情况下返回 `note-blank` 或 `note-too-large`。
28
+ 当产品界面需要让用户对单条 assistant 消息评分或加备注时,选择此服务。反馈只绑定已完成的(即已发出的)消息,并且使用该服务绝不会启动或恢复 agent。自定义应用需要把此服务与会话持久化和存储一起挂载;随附的 Web 组合已用 `maxNoteBytes: 8192` 组合好全部组件。
29
+
30
+ ### 配置
31
+
32
+ | 字段 | 默认值 | 含义 |
33
+ |---|---|---|
34
+ | `maxNoteBytes` | 必填 | 一条可选备注的最大 UTF-8 字节长度。 |
16
35
 
17
36
  ```yaml
18
37
  - id: message-feedback
@@ -21,47 +40,88 @@
21
40
  maxNoteBytes: 8192
22
41
  ```
23
42
 
24
- 服务注入 `storageDomain`、`sessionPersistence` `sessions`。其持久存储域为 `message_feedback`,其中 `sessions` 表按 `SessionId` 每个一行。
43
+ 备注必须包含至少一个非空白字符,并且不得超过配置的字节长度;空白备注会以 `note-blank` 拒绝,超长备注会以 `note-too-large` 拒绝。通过校验的文本按提交原样存储——不做任何 trim。生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-message-feedback)是每个受支持字段及其 JSDoc 的穷尽式真源。
25
44
 
26
- ## 数据、生命周期与持久性
45
+ ### 读取与修改反馈
27
46
 
28
- `MessageFeedbackItem` 包含 `messageId`、`rating: 'positive' | 'negative'`、可选 `note`、只能做相等比较的 opaque `version`,以及由 Host 分配、以 Unix 毫秒表示的 `createdAt`/`updatedAt` 时间戳。实质更新保留 `createdAt`、替换 `version`,并保证 `updatedAt` 不倒退。`list` 按首次创建顺序返回新的不可变快照;更新条目时保留其位置,删除后再创建则追加为新条目。
47
+ 调用方用三个操作读取和修改某个会话的反馈:
29
48
 
30
- 每条存储行都携带检查所得 Session header 身份 `{createdAt, cwd}`。不匹配按不存在处理:`list` 返回空 `items` 数组,`delete` 返回已不存在的后置条件,`put` 可以用绑定当前身份的新行替换陈旧行。这会在复用的 `SessionId` 具有不同 header 身份时形成隔离。fork 使用独立的 Session 身份,不复制反馈伴随记录。
49
+ | 操作 | 请求 | 成功 | 拒绝时机 |
50
+ |---|---|---|---|
51
+ | `list` | 会话 id | 当前的评分与备注,按创建顺序 | 会话不存在 |
52
+ | `put` | 会话、消息、评分、可选备注、期望的 version | 已存储的评分与备注 | 会话不存在、消息不是有效目标、version 冲突、备注空白或超长 |
53
+ | `delete` | 会话、消息、期望的 version | 评分已不存在 | 会话不存在、version 冲突 |
31
54
 
32
- `SessionPersistence.inspect()` 提供 cold-safe 观测,不发布或恢复 Agent,也不提交 cold repair。对于没有 live owner 的 Session,系统先用 `listSnapshots()` 判定明确不存在;已进入目录的 Session 若 `inspect()` 失败,仍属于基础设施故障,不会被猜测成 `session-not-found`。`put` 只接受具有指定 `MessageId` 的非空、append-origin `assistant/message`;replacement-origin 消息、仅承载 usage 的空 assistant 记录与非 assistant 记录都返回 `target-not-found`。
55
+ 每次修改都必须基于服务为该项评分返回的 version:基于更旧 version 的修改会以 `version-conflict` 拒绝,且回复携带当前评分,调用方无需再次读取即可看到变化。删除一条已经不存在的评分会成功;对不同消息的并发修改互不冲突。省略 `note` 会清除已有备注。
33
56
 
34
- 初步校验后,`put` 在写入伴随记录前建立 durability barrier。身份匹配的 live Session 先通过权威 `ctx.sessions.flush` checkpoint 提交,随后 live 与 cold 路径都会通过 `SessionPersistence.readFrom` 从序列零做物理复读。之后再次校验所得观测的 header 身份与目标。缺少 flush 参与方、身份变化、目标消失或物理读取失败都会阻止伴随记录提交,因此持久反馈绝不会先于其持久目标消息。
57
+ ### 可以对什么评分
35
58
 
36
- message feedback 不是 Session 日志内容或 Session 投影。它不发出 `feedback/record` 事件,不进入模型历史,也不触发 `FEEDBACK_ONLY` 遥测释放。
59
+ 评分绑定一条已完成的 assistant 消息:消息必须存在,并且是发送过的 assistant 消息。用户消息、空的 assistant 占位与已被替换的消息都不是有效目标,会以 `target-not-found` 拒绝。评分与备注一旦记录就与该消息绑定并跨重启保留;会话的 fork 从没有反馈开始。
37
60
 
38
- ## 服务与 Host Remote 契约
61
+ ### 持久性
39
62
 
40
- `TypertRemoteService` 与 `@Remote` 将 `MessageFeedbackService` 的同三个方法发布出去;Host endpoint 名称为 `messageFeedback.list`、`messageFeedback.put` 与 `messageFeedback.delete`。每个方法都返回判别式业务 union:`{ ok: true, value }` 或 `{ ok: false, error }`。存储、损坏或缺少 durability listener 等操作故障会产生 reject,不会被误标为业务错误。
63
+ 只有当评分所指的消息已被持久存储后,评分才会提交,因此反馈绝不会指向可能丢失的消息。读取或写入反馈绝不会启动或恢复 agent;服务直接检查已持久化的会话。
41
64
 
42
- | 方法 | 请求 | 成功 `value` | 拒绝的 `error.code` |
43
- |---|---|---|---|
44
- | `list` | `MessageFeedbackListRequest { sessionId }` | `MessageFeedbackListValue { items }` | `session-not-found` |
45
- | `put` | `MessageFeedbackPutRequest { sessionId, messageId, rating, note?, ifVersion }` | 已提交的 `MessageFeedbackItem` | `session-not-found`、`target-not-found`、`version-conflict`、`note-blank`、`note-too-large` |
46
- | `delete` | `MessageFeedbackDeleteRequest { sessionId, messageId, ifVersion }` | `MessageFeedbackDeleteValue { absent: true }` | `session-not-found`、`version-conflict` |
65
+ -----
47
66
 
48
- `MessageFeedbackVersionConflict` 返回权威 `current` 条目;条目不存在时为 `null`。调用方无需额外执行 `list`,即可协调当前 rating、note 与 version。`MessageFeedbackNoteTooLarge` 同时返回 `maxBytes` 与 `actualBytes`。客户端 Remote 聚合尚未挂载生成的客户端 contribution;Host 调用方无需该客户端组装即可使用 service/Remote 契约。
67
+ <a id="understand-the-implementation"></a>
68
+ ## 理解实现
49
69
 
50
- ## Compare-and-set 与幂等性
70
+ <details>
71
+ <summary>实现细节——点击展开</summary>
51
72
 
52
- `ifVersion: null` 表示仅当条目不存在时才创建;已有条目的每次请求都必须与其当前 version 完全一致,即使目标值已经相同、不会产生实质更新。检查按消息而非按 Session 进行,因此修改一个条目不会与另一个条目冲突。每次实质创建或更新都会分配新的 opaque UUID token,防止陈旧写入穿过 ABA 值循环。
73
+ ### 设计理念
53
74
 
54
- 携带匹配 version 的无变化请求会返回已存条目,version 与时间戳均不变。成功响应丢失后,使用旧 token 重试会得到 `version-conflict.current`;调用方无需额外读取,即可把权威当前值与目标值比较。条目已不存在时,`delete` 忽略 `ifVersion`;成功后始终返回稳定的 `{ absent: true }` 后置条件。
75
+ 服务把反馈完全放在会话日志之外:每个会话在存储域中拥有一条伴随记录行,因此评分绝不会与对话内容、模型历史或遥测混淆。伴随记录只在所引用消息已持久之后提交——该行是目标日志的延伸,而不是先于它。每个操作都返回业务结果,把已处理的失败(会话缺失、目标无效、版本陈旧、备注不合格)与基础设施故障区分开,后者会 reject 而非被误标。
55
76
 
56
- Session 划分的 promise 队列覆盖检查、持久性校验、伴随记录读取、比较与整行写入。这些语义会串行化经由同一服务实例的并发变更;storage-domain 自身没有跨进程条件写。
77
+ ### 伴随记录里有什么
57
78
 
58
- Plugin disposal 会先关闭变更接纳,排空已进入各个 Session 队列的所有操作,然后才关闭 storage domain。disposal 开始后提交的变更会以生命周期故障拒绝,不会进入正在关闭的 domain
79
+ 每个会话一行,把检查所得的会话身份(`createdAt`、`cwd`)与其反馈条目绑定;该身份隔离复用的会话 id,因此更早生命周期的行不可见,fork 也从无反馈开始。条目是不可变值——修改会写入条目新版本并保留其创建时间——行 schema 拒绝重复消息 id 与复用的版本,保证查找无歧义。精确的行 schema 与校验见 [`src/spec.ts`](src/spec.ts)
59
80
 
81
+ ### 并发
82
+
83
+ 修改是乐观的、按消息进行的:调用方发送其最后观察到的 version,陈旧的 version 会连同权威当前条目一起被拒绝,调用方无需再次读取即可协调;每次实质修改都会铸造新的 version token,陈旧写入绝不会被误认为当前。按 Session 的队列把整个读-比较-写串行化在一个服务实例内;存储不提供跨进程条件写,这正是下文「已知限制」。
84
+
85
+ ### 持久性与目标校验
86
+
87
+ 写入按「暂存—校验—提交」进行:目标消息先通过权威 checkpoint flush,再物理重读日志前缀,之后才写入伴随记录行——反馈绝不会引用尚未持久的消息。冷会话在不恢复 agent 的情况下被检查,缺失依据持久化目录判定而非猜测,只有真实发送过的 assistant 消息才是有效目标。flush 与检查路径见 [`src/index.ts`](src/index.ts)。
88
+
89
+ ### 故障模式
90
+
91
+ 服务失败时保持封闭:disposal 先排空在途写入再关闭域,disposal 开始后提交的写入会以生命周期故障拒绝,无效配置或域初始化前的读取都会明确失败。
92
+
93
+ ### 源码地图
94
+
95
+ | 文件 | 职责 |
96
+ |---|---|
97
+ | [`src/index.ts`](src/index.ts) | 服务类:配置校验、按 Session 队列、持久性屏障、`@Remote` 方法 |
98
+ | [`src/types.ts`](src/types.ts) | 公开的请求、值与失败词汇(仅类型,供生成的 Remote 客户端使用) |
99
+ | [`src/spec.ts`](src/spec.ts) | storage-domain 声明:`message_feedback` 域、`sessions` 表、行 schema |
100
+ | [`src/invariant.ts`](src/invariant.ts) | 不变式伴生插件(无运行时不变式;域 schema 在重开时校验行) |
101
+
102
+ </details>
103
+
104
+ -----
105
+
106
+ <a id="further-exploration"></a>
107
+ ## 进一步探索
108
+
109
+ 当包级约定不够用时阅读以下页面。它们从子系统类型与设计边界,逐步进入持久化原语与驱动此服务的浏览器消费方。
110
+
111
+ - [反馈子系统](../../../docs/subsystems/feedback.zh.md)——公开类型、Remote 契约与 Web 消费方细节。
112
+ - [消息反馈伴随记录决策](../../../.agents/notes/implemented/architecture/2026-08-10-message-feedback-sidecar.zh.md)——让此伴随记录不进入会话日志内容的设计边界。
113
+ - [会话持久化子系统](../../../docs/subsystems/persistence.zh.md)——持久性屏障背后的 `inspect`、`readFrom` 与 `flush` 语义。
114
+ - [dsh-client-ui-message-feedback](../../client/ui-message-feedback/README.zh.md)——驱动 Host Remote 契约的浏览器消费方。
115
+ - [反馈包映射](../README.zh.md)——逐消息反馈与仅写入日志的采集命令并存的组。
116
+
117
+ -----
118
+
119
+ <a id="model-experience"></a>
60
120
  ## 模型体验
61
121
 
62
122
  ### 本地消息反馈状态
63
123
 
64
- #### 模型看到的内容
124
+ #### 模型看到什么
65
125
 
66
126
  无。`ctx.messageFeedback` 不注册工具、提示词段落、模型可见上下文或 Session 事件;除非另一个具有独立文档的 Consumer 显式公开反馈,否则它只留在 Host 拥有的伴随记录中。
67
127
 
@@ -73,12 +133,30 @@ Plugin disposal 会先关闭变更接纳,排空已进入各个 Session 队列
73
133
 
74
134
  相互独立。读取或变更消息反馈不会触碰模型请求前缀,也不会使本可复用的提供方缓存条目失效。
75
135
 
76
- ## 已知局限与延后工作
136
+ ## 已知限制与延期工作
137
+
138
+ <a id="known-limitations-and-deferred-work"></a>
139
+
140
+
141
+ 这些限制说明服务何时不合适,或何时需要特别的运维注意。它们是当前包约束,不是任务积压。
77
142
 
78
- - **缺少客户端聚合与 UI**——Host Remote 契约已经发布,但客户端 Remote 聚合 contribution 与任何 UI 消费方由各自边界负责并保持延后。
79
143
  - **Compare-and-set 仅限单进程**——按 Session 划分的队列只串行化一个服务实例;storage-domain 不提供跨进程条件写,因此多个 Host 进程写入同一存储根目录时仍可能丢失更新。
80
- - **没有持久 Session 删除级联**——Session persistence 没有删除接口,且 `session/disposed`/`host/session-removed` 表示 detach 而非持久删除。因此服务会保留空行,并可能在带外移除日志后留下遗留行,而不会在 detach 时删除仍有效的反馈。
144
+ - **没有持久 Session 删除级联**——Session persistence 没有删除接口,且 `session/disposed`/`api-session/removed` 表示 detach 而非持久删除。因此服务会保留空行,并可能在带外移除日志后留下遗留行,而不会在 detach 时删除仍有效的反馈。
81
145
  - **Detach/catalog retirement 窗口**——请求若恰好落在 live detach 之后、persistence catalog 物化 header 之前的极短窗口,可能收到 `session-not-found`;调用方应在 retirement materialization 后重试。
82
146
  - **Header 身份不是内容指纹**——只有 `{createdAt, cwd}` 不同时才能识别复用;本契约无法区分保留相同 header 身份的克隆日志。
83
147
  - **调用方边界受信任**——`list`/`put`/`delete` 不携带已认证的 actor 或审计身份。在加入授权与归属信息前,部署方必须只通过受信任或另行认证的边界暴露 Host gateway。
84
148
  - **目录与行边界**——由于 persistence 没有按 id 读取元数据的操作,cold 请求会扫描完整的 Session snapshot 目录。`maxNoteBytes` 只限制单条备注,单个 Session 行的条目数和聚合保留字节尚无上限;按索引读取元数据和由部署决定的行边界,延后到具体消费方明确策略时处理。
149
+
150
+ <a id="dev-note"></a>
151
+ ### 开发备注
152
+
153
+ <details>
154
+ <summary>维护者的工作上下文——点击展开</summary>
155
+
156
+ 本开发备注是维护者的工作上下文,明确不具权威性。已交付的行为、限制与理由以上文、包代码与所链接的 Agent Note 为准。
157
+
158
+ - 浏览器控件与客户端 Remote 挂载位于 `dsh-client-ui-message-feedback` 与 `dsh-api-remotes`;它们的开放事项属于这些包的备注。
159
+ - 受信任调用方限制是开放的授权方向:Host gateway 不记录任何 actor 或审计身份,任何认证层都必须在部署边界落地,之后服务才能暴露按用户归属。
160
+ - 按设计,备注校验早于 Session 查找,因此对不存在的 Session,`note-blank` 与 `note-too-large` 优先于 `session-not-found`;测试固定了这一顺序。
161
+
162
+ </details>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-message-feedback",
3
3
  "description": "Lifecycle-bound per-message rating and note sidecar for the DeepSeek Harness",
4
- "version": "0.1.1-rc.2",
4
+ "version": "0.1.2-alpha.3",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -49,32 +49,32 @@
49
49
  ],
50
50
  "license": "MIT",
51
51
  "peerDependencies": {
52
- "@deepseek-ai/dsh-brand": "^0.1.1-rc.2",
53
- "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
54
- "@deepseek-ai/dsh-session": "^0.1.1-rc.2",
55
- "@deepseek-ai/dsh-session-persistence": "^0.1.1-rc.2",
56
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
57
- "@deepseek-ai/dsh-storage-domain": "^0.1.1-rc.2",
58
- "@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.2",
59
- "@deepseek-ai/cordis": "^4.0.1"
52
+ "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.3",
53
+ "@deepseek-ai/dsh-llm": "^0.1.2-alpha.3",
54
+ "@deepseek-ai/dsh-session": "^0.1.2-alpha.3",
55
+ "@deepseek-ai/dsh-session-persistence": "^0.1.2-alpha.3",
56
+ "@deepseek-ai/dsh-storage-domain": "^0.1.2-alpha.3",
57
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.2-alpha.3",
58
+ "@deepseek-ai/cordis": "^4.0.2",
59
+ "@deepseek-ai/dsh-brand": "^0.1.2-alpha.3"
60
60
  },
61
61
  "dependencies": {
62
62
  "zod": "^4.4.3",
63
- "@deepseek-ai/schemastery": "^3.18.1"
63
+ "@deepseek-ai/schemastery": "^3.18.2"
64
64
  },
65
65
  "devDependencies": {
66
- "@deepseek-ai/cordis-plugin-include": "^1.0.6",
67
- "@deepseek-ai/cordis-plugin-loader": "^1.0.2",
68
- "@deepseek-ai/dsh-brand": "^0.1.1-rc.2",
69
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
70
- "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
71
- "@deepseek-ai/dsh-session": "^0.1.1-rc.2",
72
- "@deepseek-ai/dsh-session-persistence": "^0.1.1-rc.2",
73
- "@deepseek-ai/dsh-session-persistence-jsonl": "^0.1.1-rc.2",
74
- "@deepseek-ai/dsh-storage": "^0.1.1-rc.2",
75
- "@deepseek-ai/dsh-storage-domain": "^0.1.1-rc.2",
76
- "@deepseek-ai/dsh-storage-json": "^0.1.1-rc.2",
77
- "@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.2",
78
- "@deepseek-ai/cordis": "^4.0.1"
66
+ "@deepseek-ai/cordis-plugin-include": "^1.0.7",
67
+ "@deepseek-ai/cordis-plugin-loader": "^1.0.3",
68
+ "@deepseek-ai/dsh-brand": "^0.1.2-alpha.3",
69
+ "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.3",
70
+ "@deepseek-ai/dsh-llm": "^0.1.2-alpha.3",
71
+ "@deepseek-ai/dsh-session-persistence": "^0.1.2-alpha.3",
72
+ "@deepseek-ai/dsh-session-persistence-jsonl": "^0.1.2-alpha.3",
73
+ "@deepseek-ai/dsh-storage": "^0.1.2-alpha.3",
74
+ "@deepseek-ai/dsh-storage-domain": "^0.1.2-alpha.3",
75
+ "@deepseek-ai/dsh-storage-json": "^0.1.2-alpha.3",
76
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.2-alpha.3",
77
+ "@deepseek-ai/cordis": "^4.0.2",
78
+ "@deepseek-ai/dsh-session": "^0.1.2-alpha.3"
79
79
  }
80
80
  }