fix(feedback): address backend review gaps
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# 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 docs/subsystems/feedback.md
|
||||
feedback.md: e5c99ee639403ec3da0cf5845ec00f5ea1a5817d
|
||||
feedback.zh.md: 407d111da59af1d9a5c99c0e3d06d70df9051a13
|
||||
feedback.md: 76a29f7d6ba604fa07ed56429c9b066e22639671
|
||||
feedback.zh.md: 5a409832de68b6d0bc9688a907c0f22edd3b0a43
|
||||
|
||||
@@ -6,6 +6,183 @@ English | [中文](feedback.zh.md)
|
||||
|
||||
Source: [`packages/feedback/message-feedback/src/types.ts`](../../packages/feedback/message-feedback/src/types.ts)
|
||||
|
||||
## Public types
|
||||
|
||||
```ts type-equiv
|
||||
/** Opaque compare-and-set token for one exact feedback item revision. */
|
||||
type MessageFeedbackVersion = Branded<'MessageFeedbackVersion'>
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** The human's overall judgment of one assistant message. */
|
||||
type MessageFeedbackRating = 'positive' | 'negative'
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** One current feedback value and its opaque mutation token. */
|
||||
interface MessageFeedbackItem {
|
||||
/** Stable identity of the assistant message inside the owning Session. */
|
||||
readonly messageId: MessageId
|
||||
/** Overall positive or negative judgment. */
|
||||
readonly rating: MessageFeedbackRating
|
||||
/** Optional explanation, preserved verbatim after validation. */
|
||||
readonly note?: string
|
||||
/** Equality-only token replaced by every material create or update. */
|
||||
readonly version: MessageFeedbackVersion
|
||||
/** Host-assigned creation time in Unix epoch milliseconds. */
|
||||
readonly createdAt: number
|
||||
/** Host-assigned time of the most recent material update. */
|
||||
readonly updatedAt: number
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Read all message feedback belonging to one persisted Session lifecycle. */
|
||||
interface MessageFeedbackListRequest {
|
||||
/** Persisted Session whose sidecar should be read. */
|
||||
readonly sessionId: SessionId
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Current feedback values for one Session, in first-creation order. */
|
||||
interface MessageFeedbackListValue {
|
||||
/** Fresh immutable item snapshots. */
|
||||
readonly items: readonly MessageFeedbackItem[]
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Create or replace feedback for one assistant message. */
|
||||
interface MessageFeedbackPutRequest {
|
||||
/** Persisted Session that owns the target message. */
|
||||
readonly sessionId: SessionId
|
||||
/** Target assistant-message identity. */
|
||||
readonly messageId: MessageId
|
||||
/** Desired overall judgment. */
|
||||
readonly rating: MessageFeedbackRating
|
||||
/** Optional non-blank explanation. */
|
||||
readonly note?: string
|
||||
/** Observed item version, or `null` to require that no item exists. */
|
||||
readonly ifVersion: MessageFeedbackVersion | null
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Delete feedback for one message after observing its current version. */
|
||||
interface MessageFeedbackDeleteRequest {
|
||||
/** Persisted Session that owns the sidecar. */
|
||||
readonly sessionId: SessionId
|
||||
/** Message whose feedback should be absent after this operation. */
|
||||
readonly messageId: MessageId
|
||||
/** Observed item version; ignored when the item is already absent. */
|
||||
readonly ifVersion: MessageFeedbackVersion
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Idempotent deletion acknowledgement. */
|
||||
interface MessageFeedbackDeleteValue {
|
||||
/** Stable postcondition shared by the first deletion and every retry. */
|
||||
readonly absent: true
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** No persisted Session header exists for the requested id. */
|
||||
interface MessageFeedbackSessionNotFound {
|
||||
readonly code: 'session-not-found'
|
||||
readonly sessionId: SessionId
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** The id does not name a derived, append-origin assistant message. */
|
||||
interface MessageFeedbackTargetNotFound {
|
||||
readonly code: 'target-not-found'
|
||||
readonly sessionId: SessionId
|
||||
readonly messageId: MessageId
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** A material mutation did not match the addressed item's current version. */
|
||||
interface MessageFeedbackVersionConflict {
|
||||
readonly code: 'version-conflict'
|
||||
/** Authoritative current item, or `null` when it does not exist. */
|
||||
readonly current: MessageFeedbackItem | null
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** A supplied note contains no non-whitespace character. */
|
||||
interface MessageFeedbackNoteBlank {
|
||||
readonly code: 'note-blank'
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** A supplied note exceeds the configured UTF-8 byte limit. */
|
||||
interface MessageFeedbackNoteTooLarge {
|
||||
readonly code: 'note-too-large'
|
||||
readonly maxBytes: number
|
||||
readonly actualBytes: number
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Failures shared by the public message-feedback operations. */
|
||||
type MessageFeedbackFailure =
|
||||
| MessageFeedbackSessionNotFound
|
||||
| MessageFeedbackTargetNotFound
|
||||
| MessageFeedbackVersionConflict
|
||||
| MessageFeedbackNoteBlank
|
||||
| MessageFeedbackNoteTooLarge
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Successful public operation result. */
|
||||
interface MessageFeedbackSuccess<T> {
|
||||
readonly ok: true
|
||||
readonly value: T
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Rejected public operation result with a stable business failure. */
|
||||
interface MessageFeedbackRejected<E extends MessageFeedbackFailure> {
|
||||
readonly ok: false
|
||||
readonly error: E
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Result returned by the message-feedback `list` operation. */
|
||||
type MessageFeedbackListResult =
|
||||
| MessageFeedbackSuccess<MessageFeedbackListValue>
|
||||
| MessageFeedbackRejected<MessageFeedbackSessionNotFound>
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Result returned by the message-feedback `put` operation. */
|
||||
type MessageFeedbackPutResult =
|
||||
| MessageFeedbackSuccess<MessageFeedbackItem>
|
||||
| MessageFeedbackRejected<
|
||||
| MessageFeedbackSessionNotFound
|
||||
| MessageFeedbackTargetNotFound
|
||||
| MessageFeedbackVersionConflict
|
||||
| MessageFeedbackNoteBlank
|
||||
| MessageFeedbackNoteTooLarge
|
||||
>
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Result returned by the message-feedback `delete` operation. */
|
||||
type MessageFeedbackDeleteResult =
|
||||
| MessageFeedbackSuccess<MessageFeedbackDeleteValue>
|
||||
| MessageFeedbackRejected<MessageFeedbackSessionNotFound | MessageFeedbackVersionConflict>
|
||||
```
|
||||
|
||||
## Data and concurrency
|
||||
|
||||
One Session sidecar row contains its header identity `{createdAt, cwd}` and feedback items keyed by `MessageId`. Each item carries a positive or negative rating, an optional note, Host-assigned `createdAt`/`updatedAt` timestamps, and its own opaque version. Versions are compared only for equality and only against the addressed message; callers do not order or synthesize them.
|
||||
@@ -22,12 +199,15 @@ The stored `{createdAt, cwd}` identity must match the inspected header. A mismat
|
||||
|
||||
The service stores whole Session rows in the `message_feedback` storage domain through `ctx.storageDomain`. Before `put` commits a row that references a target message, a matching live target passes through the canonical `ctx.sessions.flush` checkpoint; both live and cold paths are then physically read from sequence zero through `SessionPersistence.readFrom`. The resulting observation is revalidated before the sidecar write, so the durable target log always precedes its sidecar commit. `maxNoteBytes` is required and bounds note text by UTF-8 bytes; the Web Host composition sets `8192`. The package publishes the Host `messageFeedback.list`, `messageFeedback.put`, and `messageFeedback.delete` unary Remote contract through `GatewayService` and `@Remote`; the generated Cordis surface below is the method-level authority.
|
||||
|
||||
Plugin disposal closes mutation admission, drains accepted per-Session queue work, and then closes the storage domain.
|
||||
|
||||
## Boundaries and limitations
|
||||
|
||||
- The client Remote aggregate mount and UI consumer are separately owned and deferred.
|
||||
- The mutation queue is process-local. Storage-domain has no cross-process conditional write, so multiple Host writers to one storage root have no compare-and-swap or lost-update guarantee.
|
||||
- Session persistence has no durable deletion surface. The service does not treat `session/disposed` or `host/session-removed` as deletion and therefore performs no fake cascade; orphan sidecar rows may remain after out-of-band log removal.
|
||||
- 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.
|
||||
- Cold requests scan the complete Session snapshot catalog because persistence has no lookup-by-id metadata operation. One Session row also has no item-count or aggregate-byte cap; `maxNoteBytes` bounds only each note until a concrete consumer owns a row policy.
|
||||
- Header identity detects a reused id only when `{createdAt, cwd}` differs; a cloned log retaining the same header identity is indistinguishable by this contract.
|
||||
- The Host contract records no authenticated actor or audit identity and therefore assumes a trusted caller boundary.
|
||||
|
||||
|
||||
@@ -6,6 +6,183 @@
|
||||
|
||||
来源:[`packages/feedback/message-feedback/src/types.ts`](../../packages/feedback/message-feedback/src/types.ts)
|
||||
|
||||
## 公开类型
|
||||
|
||||
```ts type-equiv
|
||||
/** Opaque compare-and-set token for one exact feedback item revision. */
|
||||
type MessageFeedbackVersion = Branded<'MessageFeedbackVersion'>
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** The human's overall judgment of one assistant message. */
|
||||
type MessageFeedbackRating = 'positive' | 'negative'
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** One current feedback value and its opaque mutation token. */
|
||||
interface MessageFeedbackItem {
|
||||
/** Stable identity of the assistant message inside the owning Session. */
|
||||
readonly messageId: MessageId
|
||||
/** Overall positive or negative judgment. */
|
||||
readonly rating: MessageFeedbackRating
|
||||
/** Optional explanation, preserved verbatim after validation. */
|
||||
readonly note?: string
|
||||
/** Equality-only token replaced by every material create or update. */
|
||||
readonly version: MessageFeedbackVersion
|
||||
/** Host-assigned creation time in Unix epoch milliseconds. */
|
||||
readonly createdAt: number
|
||||
/** Host-assigned time of the most recent material update. */
|
||||
readonly updatedAt: number
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Read all message feedback belonging to one persisted Session lifecycle. */
|
||||
interface MessageFeedbackListRequest {
|
||||
/** Persisted Session whose sidecar should be read. */
|
||||
readonly sessionId: SessionId
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Current feedback values for one Session, in first-creation order. */
|
||||
interface MessageFeedbackListValue {
|
||||
/** Fresh immutable item snapshots. */
|
||||
readonly items: readonly MessageFeedbackItem[]
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Create or replace feedback for one assistant message. */
|
||||
interface MessageFeedbackPutRequest {
|
||||
/** Persisted Session that owns the target message. */
|
||||
readonly sessionId: SessionId
|
||||
/** Target assistant-message identity. */
|
||||
readonly messageId: MessageId
|
||||
/** Desired overall judgment. */
|
||||
readonly rating: MessageFeedbackRating
|
||||
/** Optional non-blank explanation. */
|
||||
readonly note?: string
|
||||
/** Observed item version, or `null` to require that no item exists. */
|
||||
readonly ifVersion: MessageFeedbackVersion | null
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Delete feedback for one message after observing its current version. */
|
||||
interface MessageFeedbackDeleteRequest {
|
||||
/** Persisted Session that owns the sidecar. */
|
||||
readonly sessionId: SessionId
|
||||
/** Message whose feedback should be absent after this operation. */
|
||||
readonly messageId: MessageId
|
||||
/** Observed item version; ignored when the item is already absent. */
|
||||
readonly ifVersion: MessageFeedbackVersion
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Idempotent deletion acknowledgement. */
|
||||
interface MessageFeedbackDeleteValue {
|
||||
/** Stable postcondition shared by the first deletion and every retry. */
|
||||
readonly absent: true
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** No persisted Session header exists for the requested id. */
|
||||
interface MessageFeedbackSessionNotFound {
|
||||
readonly code: 'session-not-found'
|
||||
readonly sessionId: SessionId
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** The id does not name a derived, append-origin assistant message. */
|
||||
interface MessageFeedbackTargetNotFound {
|
||||
readonly code: 'target-not-found'
|
||||
readonly sessionId: SessionId
|
||||
readonly messageId: MessageId
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** A material mutation did not match the addressed item's current version. */
|
||||
interface MessageFeedbackVersionConflict {
|
||||
readonly code: 'version-conflict'
|
||||
/** Authoritative current item, or `null` when it does not exist. */
|
||||
readonly current: MessageFeedbackItem | null
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** A supplied note contains no non-whitespace character. */
|
||||
interface MessageFeedbackNoteBlank {
|
||||
readonly code: 'note-blank'
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** A supplied note exceeds the configured UTF-8 byte limit. */
|
||||
interface MessageFeedbackNoteTooLarge {
|
||||
readonly code: 'note-too-large'
|
||||
readonly maxBytes: number
|
||||
readonly actualBytes: number
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Failures shared by the public message-feedback operations. */
|
||||
type MessageFeedbackFailure =
|
||||
| MessageFeedbackSessionNotFound
|
||||
| MessageFeedbackTargetNotFound
|
||||
| MessageFeedbackVersionConflict
|
||||
| MessageFeedbackNoteBlank
|
||||
| MessageFeedbackNoteTooLarge
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Successful public operation result. */
|
||||
interface MessageFeedbackSuccess<T> {
|
||||
readonly ok: true
|
||||
readonly value: T
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Rejected public operation result with a stable business failure. */
|
||||
interface MessageFeedbackRejected<E extends MessageFeedbackFailure> {
|
||||
readonly ok: false
|
||||
readonly error: E
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Result returned by the message-feedback `list` operation. */
|
||||
type MessageFeedbackListResult =
|
||||
| MessageFeedbackSuccess<MessageFeedbackListValue>
|
||||
| MessageFeedbackRejected<MessageFeedbackSessionNotFound>
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Result returned by the message-feedback `put` operation. */
|
||||
type MessageFeedbackPutResult =
|
||||
| MessageFeedbackSuccess<MessageFeedbackItem>
|
||||
| MessageFeedbackRejected<
|
||||
| MessageFeedbackSessionNotFound
|
||||
| MessageFeedbackTargetNotFound
|
||||
| MessageFeedbackVersionConflict
|
||||
| MessageFeedbackNoteBlank
|
||||
| MessageFeedbackNoteTooLarge
|
||||
>
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Result returned by the message-feedback `delete` operation. */
|
||||
type MessageFeedbackDeleteResult =
|
||||
| MessageFeedbackSuccess<MessageFeedbackDeleteValue>
|
||||
| MessageFeedbackRejected<MessageFeedbackSessionNotFound | MessageFeedbackVersionConflict>
|
||||
```
|
||||
|
||||
## 数据与并发
|
||||
|
||||
每个 Session 的一条伴随记录包含 header 身份 `{createdAt, cwd}` 和以 `MessageId` 为键的反馈条目。每个条目携带好评或差评、可选备注、Host 分配的 `createdAt`/`updatedAt` 时间戳及自己的 opaque version。version 只能用于相等比较,且只与目标消息比较;调用方不能排序或自行合成它。
|
||||
@@ -22,12 +199,15 @@
|
||||
|
||||
服务通过 `ctx.storageDomain` 在 `message_feedback` 存储域中保存完整 Session 行。`put` 提交引用目标消息的伴随记录前,身份匹配的 live 目标先经过权威 `ctx.sessions.flush` checkpoint;随后 live 与 cold 路径都会通过 `SessionPersistence.readFrom` 从序列零做物理复读。写入伴随记录前会再次校验所得观测,因此目标日志的持久提交始终先于其伴随记录。`maxNoteBytes` 为必填项,按 UTF-8 字节限制备注文本;Web Host 组合将其设为 `8192`。该包通过 `GatewayService` 与 `@Remote` 发布 Host `messageFeedback.list`、`messageFeedback.put` 和 `messageFeedback.delete` 一元 Remote 契约;下方生成的 Cordis surface 是方法级权威。
|
||||
|
||||
Plugin disposal 会先关闭变更接纳,排空已进入各 Session 队列的工作,然后才关闭 storage domain。
|
||||
|
||||
## 边界与限制
|
||||
|
||||
- 客户端 Remote 聚合挂载与 UI 消费方由各自边界负责并保持延后。
|
||||
- 变更队列仅在进程内生效。storage-domain 没有跨进程条件写,因此多个 Host 写入同一存储根目录时,不提供 compare-and-swap 或防止丢失更新的保证。
|
||||
- Session persistence 没有持久删除接口。服务不把 `session/disposed` 或 `host/session-removed` 当作删除,因此不伪造级联;在带外移除日志后,孤儿伴随记录可能继续存在。
|
||||
- 请求若恰好落在 live detach 之后、persistence catalog 物化 header 之前的极短窗口,可能收到 `session-not-found`;调用方应在 retirement materialization 后重试。
|
||||
- 由于 persistence 没有按 id 读取元数据的操作,cold 请求会扫描完整的 Session snapshot 目录。单个 Session 行也没有条目数或聚合字节上限;在具体消费方拥有行策略之前,`maxNoteBytes` 只限制每条备注。
|
||||
- 只有 `{createdAt, cwd}` 不同时,header 身份才能识别复用的 id;本契约无法区分保留相同 header 身份的克隆日志。
|
||||
- Host 契约不记录已认证的 actor 或审计身份,因此假设调用方边界可信。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user