From c61be607373f58aeac1e75a7e7a48bafd739620c Mon Sep 17 00:00:00 2001 From: ZiyaZhang <199893125+ZiyaZhang@users.noreply.github.com> Date: Mon, 10 Aug 2026 21:18:45 -0700 Subject: [PATCH] fix(feedback): address backend review gaps --- ...6-08-10-message-feedback-sidecar.i18n.yaml | 4 +- .../2026-08-10-message-feedback-sidecar.md | 2 +- .../2026-08-10-message-feedback-sidecar.zh.md | 2 +- .../message-feedback-protocol.snapshot.ts | 115 ++++++++++ .../protocol.expected.json | 203 ++++++++++++++++++ .../message-feedback-protocol/session.jsonl | 7 + apps/web/tsconfig.json | 1 + docs/subsystems/feedback.i18n.yaml | 4 +- docs/subsystems/feedback.md | 180 ++++++++++++++++ docs/subsystems/feedback.zh.md | 180 ++++++++++++++++ .../message-feedback/README.i18n.yaml | 4 +- packages/feedback/message-feedback/README.md | 5 +- .../feedback/message-feedback/README.zh.md | 5 +- .../feedback/message-feedback/src/index.ts | 10 +- .../feedback/message-feedback/src/spec.ts | 2 + .../message-feedback/tests/helpers.ts | 15 +- .../message-feedback/tests/invariant.spec.ts | 23 ++ .../tests/message-feedback.spec.ts | 51 +++++ scripts/type-equiv.manifest.json | 95 ++++++++ tsconfig.host.json | 1 + 20 files changed, 893 insertions(+), 16 deletions(-) create mode 100644 apps/web/tests/message-feedback-protocol.snapshot.ts create mode 100644 apps/web/tests/snapshots/message-feedback-protocol/protocol.expected.json create mode 100644 apps/web/tests/snapshots/message-feedback-protocol/session.jsonl create mode 100644 packages/feedback/message-feedback/tests/invariant.spec.ts diff --git a/.agents/notes/implemented/architecture/2026-08-10-message-feedback-sidecar.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-10-message-feedback-sidecar.i18n.yaml index ec3d38f09c..20b1a4cfd5 100644 --- a/.agents/notes/implemented/architecture/2026-08-10-message-feedback-sidecar.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-10-message-feedback-sidecar.i18n.yaml @@ -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 .agents/notes/implemented/architecture/2026-08-10-message-feedback-sidecar.md -2026-08-10-message-feedback-sidecar.md: a741a7e6b9fe422c9aafb68d9b099466399e1ee9 -2026-08-10-message-feedback-sidecar.zh.md: da176e13586bb93ebadcefc0afb44c21ce386e08 +2026-08-10-message-feedback-sidecar.md: 780cbaa840fcac7bcfa799468bfd61f5b08715cb +2026-08-10-message-feedback-sidecar.zh.md: 72ecc82717010f65d013418a45c3b38c6084aaf9 diff --git a/.agents/notes/implemented/architecture/2026-08-10-message-feedback-sidecar.md b/.agents/notes/implemented/architecture/2026-08-10-message-feedback-sidecar.md index a741a7e6b9..780cbaa840 100644 --- a/.agents/notes/implemented/architecture/2026-08-10-message-feedback-sidecar.md +++ b/.agents/notes/implemented/architecture/2026-08-10-message-feedback-sidecar.md @@ -22,7 +22,7 @@ Before `put` commits a sidecar row, it puts the target log behind a durability b Each message item carries its own opaque version plus Host-assigned `createdAt` and `updatedAt` timestamps. `put` compares the caller's `ifVersion` only with the addressed item, so editing one message does not invalidate another. The comparison is strict even when the desired value already matches, preventing a stale request from crossing an ABA value cycle; a conflict returns the authoritative current item so callers can reconcile without a second read. A matching-version no-op preserves the version and timestamps, while a material update preserves `createdAt`, replaces the version, and keeps `updatedAt` from moving backward. An already-absent delete is likewise successful. Versions are tokens for equality, not counters callers may order or synthesize. -A per-Session mutation queue encloses lifecycle inspection, sidecar read, conflict evaluation, and whole-row write. This makes one service instance's mutations serial and preserves the per-message compare-and-swap contract inside one Host process. The underlying storage-domain API provides no cross-process conditional write, so the implementation claims no cross-process linearizability or lost-update protection. +A per-Session mutation queue encloses lifecycle inspection, sidecar read, conflict evaluation, and whole-row write. This makes one service instance's mutations serial and preserves the per-message compare-and-swap contract inside one Host process. Plugin disposal closes admission, drains accepted queue work, and then closes the storage domain. The underlying storage-domain API provides no cross-process conditional write, so the implementation claims no cross-process linearizability or lost-update protection. `maxNoteBytes` is a required deployment choice and bounds the UTF-8 byte length of an optional note; the Web Host bundle sets it explicitly to `8192`. The package publishes the Host `messageFeedback.list`, `messageFeedback.put`, and `messageFeedback.delete` contract directly through `GatewayService` and `@Remote`. Client Remote aggregate mounting and UI remain separately owned and deferred; their later adapter stays a thin consumer of this Host contract. diff --git a/.agents/notes/implemented/architecture/2026-08-10-message-feedback-sidecar.zh.md b/.agents/notes/implemented/architecture/2026-08-10-message-feedback-sidecar.zh.md index da176e1358..72ecc82717 100644 --- a/.agents/notes/implemented/architecture/2026-08-10-message-feedback-sidecar.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-10-message-feedback-sidecar.zh.md @@ -22,7 +22,7 @@ Status: implemented 每个消息条目都携带自己的 opaque version,以及 Host 分配的 `createdAt` 和 `updatedAt` 时间戳。`put` 只把调用方的 `ifVersion` 与目标条目比较,因此编辑一条消息不会使另一条消息失效。即使目标值已经相同,比较仍然严格执行,从而防止陈旧请求穿过 ABA 值循环;冲突会返回权威当前条目,调用方无需二次读取即可协调。携带匹配 version 的无变化请求会保留 version 与时间戳;实质更新保留 `createdAt`、替换 version,并保证 `updatedAt` 不倒退。删除已经不存在的条目也同样成功。version 是只能做相等比较的 token,不是调用方可以排序或自行合成的计数器。 -按 Session 划分的变更队列覆盖生命周期检查、伴随记录读取、冲突判断与整行写入。这使同一个服务实例的变更串行化,并在单个 Host 进程内保持逐消息 compare-and-swap 契约。底层 storage-domain API 不提供跨进程条件写,因此实现不承诺跨进程线性一致性或防止丢失更新。 +按 Session 划分的变更队列覆盖生命周期检查、伴随记录读取、冲突判断与整行写入。这使同一个服务实例的变更串行化,并在单个 Host 进程内保持逐消息 compare-and-swap 契约。Plugin disposal 会关闭接纳、排空已进入队列的工作,然后关闭 storage domain。底层 storage-domain API 不提供跨进程条件写,因此实现不承诺跨进程线性一致性或防止丢失更新。 `maxNoteBytes` 是必填的部署选择,用于限制可选备注的 UTF-8 字节长度;Web Host bundle 将其显式设为 `8192`。该包通过 `GatewayService` 与 `@Remote` 直接发布 Host `messageFeedback.list`、`messageFeedback.put` 与 `messageFeedback.delete` 契约。客户端 Remote 聚合挂载与 UI 由各自边界负责并保持延后;后续适配层只是该 Host 契约的薄消费者。 diff --git a/apps/web/tests/message-feedback-protocol.snapshot.ts b/apps/web/tests/message-feedback-protocol.snapshot.ts new file mode 100644 index 0000000000..9fbc556f77 --- /dev/null +++ b/apps/web/tests/message-feedback-protocol.snapshot.ts @@ -0,0 +1,115 @@ +import { readFile } from 'node:fs/promises' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import { + assertFixtureInventory, + compareOrRefreshGolden, + launchWebScaffold, + seedSession, + type WebScaffold, +} from './scaffold.ts' + +const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/message-feedback-protocol', import.meta.url)) +const SESSION_FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl') +const PROTOCOL_EXPECTED = join(SNAPSHOT_DIR, 'protocol.expected.json') +const SESSION_ID = 'message-feedback-protocol' +const MESSAGE_ID = '11111111-1111-4111-8111-111111111111' + +interface ProtocolExchange { + readonly endpoint: string + readonly request: unknown + readonly status: number + readonly response: unknown +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null +} + +/** Extract the opaque item version while keeping every surrounding wire field snapshot-owned. */ +function createdVersion(response: unknown): string { + if (!isRecord(response) || !isRecord(response.result) || response.result.ok !== true + || !isRecord(response.result.value) || response.result.value.ok !== true + || !isRecord(response.result.value.value) + || typeof response.result.value.value.version !== 'string') { + throw new Error('messageFeedback.put did not return a successful versioned item') + } + return response.result.value.value.version +} + +/** Replace only run-owned UUID/time values; all protocol names and business fields stay exact. */ +function normalizeProtocol(exchanges: readonly ProtocolExchange[], version: string): string { + return JSON.stringify(exchanges, (key, value: unknown) => { + if ((key === 'version' || key === 'ifVersion') && value === version) return '{{version}}' + if ((key === 'createdAt' || key === 'updatedAt') && typeof value === 'number') return '{{timestamp}}' + return value + }, 2) +} + +describe('message feedback Host Remote protocol', () => { + let scaffold: WebScaffold + + beforeAll(async () => { + scaffold = await launchWebScaffold() + await seedSession(scaffold, await readFile(SESSION_FIXTURE, 'utf8'), SESSION_ID) + }) + + afterAll(async () => { + await scaffold?.close() + }) + + it('snapshots strict list, put, conflict, and delete calls through the shipped Web Host', async () => { + const exchanges: ProtocolExchange[] = [] + const invoke = async (rpcId: string, endpoint: string, request: unknown): Promise => { + const payload = { args: { request } } + const response = await fetch(`${scaffold.baseUrl}/api/${endpoint}`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + type: 'client-request', + rpcId, + method: endpoint, + payload, + }), + }) + const body: unknown = await response.json() + exchanges.push({ endpoint: `/api/${endpoint}`, request: payload, status: response.status, response: body }) + return body + } + + await invoke('feedback-invalid', 'messageFeedback/put', { + sessionId: SESSION_ID, + messageId: MESSAGE_ID, + rating: 'invalid-rating', + ifVersion: null, + }) + await invoke('feedback-list-empty', 'messageFeedback/list', { sessionId: SESSION_ID }) + const created = await invoke('feedback-put', 'messageFeedback/put', { + sessionId: SESSION_ID, + messageId: MESSAGE_ID, + rating: 'positive', + note: 'Useful answer', + ifVersion: null, + }) + const version = createdVersion(created) + expect(version).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/) + await invoke('feedback-list-created', 'messageFeedback/list', { sessionId: SESSION_ID }) + await invoke('feedback-conflict', 'messageFeedback/put', { + sessionId: SESSION_ID, + messageId: MESSAGE_ID, + rating: 'negative', + ifVersion: null, + }) + await invoke('feedback-delete', 'messageFeedback/delete', { + sessionId: SESSION_ID, + messageId: MESSAGE_ID, + ifVersion: version, + }) + await invoke('feedback-list-deleted', 'messageFeedback/list', { sessionId: SESSION_ID }) + + expect(exchanges.every(exchange => exchange.status === 200)).toBe(true) + await compareOrRefreshGolden(PROTOCOL_EXPECTED, normalizeProtocol(exchanges, version), scaffold.mode) + await assertFixtureInventory(SNAPSHOT_DIR, ['protocol.expected.json', 'session.jsonl']) + }) +}) diff --git a/apps/web/tests/snapshots/message-feedback-protocol/protocol.expected.json b/apps/web/tests/snapshots/message-feedback-protocol/protocol.expected.json new file mode 100644 index 0000000000..302e4876df --- /dev/null +++ b/apps/web/tests/snapshots/message-feedback-protocol/protocol.expected.json @@ -0,0 +1,203 @@ +[ + { + "endpoint": "/api/messageFeedback/put", + "request": { + "args": { + "request": { + "sessionId": "message-feedback-protocol", + "messageId": "11111111-1111-4111-8111-111111111111", + "rating": "invalid-rating", + "ifVersion": null + } + } + }, + "status": 200, + "response": { + "type": "server-response", + "rpcId": "feedback-invalid", + "result": { + "ok": false, + "error": { + "code": "internal", + "message": "typert gateway: messageFeedback/put: wire field \"request\" failed boundary validation", + "details": {} + } + } + } + }, + { + "endpoint": "/api/messageFeedback/list", + "request": { + "args": { + "request": { + "sessionId": "message-feedback-protocol" + } + } + }, + "status": 200, + "response": { + "type": "server-response", + "rpcId": "feedback-list-empty", + "result": { + "ok": true, + "value": { + "ok": true, + "value": { + "items": [] + } + } + } + } + }, + { + "endpoint": "/api/messageFeedback/put", + "request": { + "args": { + "request": { + "sessionId": "message-feedback-protocol", + "messageId": "11111111-1111-4111-8111-111111111111", + "rating": "positive", + "note": "Useful answer", + "ifVersion": null + } + } + }, + "status": 200, + "response": { + "type": "server-response", + "rpcId": "feedback-put", + "result": { + "ok": true, + "value": { + "ok": true, + "value": { + "messageId": "11111111-1111-4111-8111-111111111111", + "rating": "positive", + "note": "Useful answer", + "version": "{{version}}", + "createdAt": "{{timestamp}}", + "updatedAt": "{{timestamp}}" + } + } + } + } + }, + { + "endpoint": "/api/messageFeedback/list", + "request": { + "args": { + "request": { + "sessionId": "message-feedback-protocol" + } + } + }, + "status": 200, + "response": { + "type": "server-response", + "rpcId": "feedback-list-created", + "result": { + "ok": true, + "value": { + "ok": true, + "value": { + "items": [ + { + "messageId": "11111111-1111-4111-8111-111111111111", + "rating": "positive", + "note": "Useful answer", + "version": "{{version}}", + "createdAt": "{{timestamp}}", + "updatedAt": "{{timestamp}}" + } + ] + } + } + } + } + }, + { + "endpoint": "/api/messageFeedback/put", + "request": { + "args": { + "request": { + "sessionId": "message-feedback-protocol", + "messageId": "11111111-1111-4111-8111-111111111111", + "rating": "negative", + "ifVersion": null + } + } + }, + "status": 200, + "response": { + "type": "server-response", + "rpcId": "feedback-conflict", + "result": { + "ok": true, + "value": { + "ok": false, + "error": { + "code": "version-conflict", + "current": { + "messageId": "11111111-1111-4111-8111-111111111111", + "rating": "positive", + "note": "Useful answer", + "version": "{{version}}", + "createdAt": "{{timestamp}}", + "updatedAt": "{{timestamp}}" + } + } + } + } + } + }, + { + "endpoint": "/api/messageFeedback/delete", + "request": { + "args": { + "request": { + "sessionId": "message-feedback-protocol", + "messageId": "11111111-1111-4111-8111-111111111111", + "ifVersion": "{{version}}" + } + } + }, + "status": 200, + "response": { + "type": "server-response", + "rpcId": "feedback-delete", + "result": { + "ok": true, + "value": { + "ok": true, + "value": { + "absent": true + } + } + } + } + }, + { + "endpoint": "/api/messageFeedback/list", + "request": { + "args": { + "request": { + "sessionId": "message-feedback-protocol" + } + } + }, + "status": 200, + "response": { + "type": "server-response", + "rpcId": "feedback-list-deleted", + "result": { + "ok": true, + "value": { + "ok": true, + "value": { + "items": [] + } + } + } + } + } +] diff --git a/apps/web/tests/snapshots/message-feedback-protocol/session.jsonl b/apps/web/tests/snapshots/message-feedback-protocol/session.jsonl new file mode 100644 index 0000000000..d970fb8cb4 --- /dev/null +++ b/apps/web/tests/snapshots/message-feedback-protocol/session.jsonl @@ -0,0 +1,7 @@ +{"type":"session","version":0,"id":"{{sessionId}}","createdAt":1786406400000,"cwd":"{{cwd}}"} +{"type":"turn/start","seq":0,"time":1786406400001,"data":{"turn":1}} +{"type":"user/message","seq":1,"time":1786406400002,"data":{"role":"user","content":[{"type":"text","text":"Give one useful answer."}],"source":{"kind":"user"},"id":"22222222-2222-4222-8222-222222222222"},"surfaceOp":"append"} +{"type":"step/start","seq":2,"time":1786406400003,"data":{"turn":1,"step":1}} +{"type":"assistant/message","seq":3,"time":1786406400004,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"A useful answer."}],"source":{"kind":"model","provider":"fixture","model":"fixture"},"id":"11111111-1111-4111-8111-111111111111"},"usage":{"inputTokens":4,"outputTokens":4}},"surfaceOp":"append"} +{"type":"step/end","seq":4,"time":1786406400005,"data":{"turn":1,"step":1}} +{"type":"turn/end","seq":5,"time":1786406400006,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index fc1a777afc..8147982475 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -25,6 +25,7 @@ "tests/scaffold.ts", "tests/scaffold-hermetic.e2e.ts", "tests/minimal-preset.snapshot.ts", + "tests/message-feedback-protocol.snapshot.ts", "tests/live-interactions.e2e.ts", "tests/question-composer.e2e.ts", "tests/approval-composer.e2e.ts", diff --git a/docs/subsystems/feedback.i18n.yaml b/docs/subsystems/feedback.i18n.yaml index 681e3c2c60..f6de2debe9 100644 --- a/docs/subsystems/feedback.i18n.yaml +++ b/docs/subsystems/feedback.i18n.yaml @@ -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 diff --git a/docs/subsystems/feedback.md b/docs/subsystems/feedback.md index e5c99ee639..76a29f7d6b 100644 --- a/docs/subsystems/feedback.md +++ b/docs/subsystems/feedback.md @@ -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 { + readonly ok: true + readonly value: T +} +``` + +```ts type-equiv +/** Rejected public operation result with a stable business failure. */ +interface MessageFeedbackRejected { + readonly ok: false + readonly error: E +} +``` + +```ts type-equiv +/** Result returned by the message-feedback `list` operation. */ +type MessageFeedbackListResult = + | MessageFeedbackSuccess + | MessageFeedbackRejected +``` + +```ts type-equiv +/** Result returned by the message-feedback `put` operation. */ +type MessageFeedbackPutResult = + | MessageFeedbackSuccess + | MessageFeedbackRejected< + | MessageFeedbackSessionNotFound + | MessageFeedbackTargetNotFound + | MessageFeedbackVersionConflict + | MessageFeedbackNoteBlank + | MessageFeedbackNoteTooLarge + > +``` + +```ts type-equiv +/** Result returned by the message-feedback `delete` operation. */ +type MessageFeedbackDeleteResult = + | MessageFeedbackSuccess + | MessageFeedbackRejected +``` + ## 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. diff --git a/docs/subsystems/feedback.zh.md b/docs/subsystems/feedback.zh.md index 407d111da5..5a409832de 100644 --- a/docs/subsystems/feedback.zh.md +++ b/docs/subsystems/feedback.zh.md @@ -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 { + readonly ok: true + readonly value: T +} +``` + +```ts type-equiv +/** Rejected public operation result with a stable business failure. */ +interface MessageFeedbackRejected { + readonly ok: false + readonly error: E +} +``` + +```ts type-equiv +/** Result returned by the message-feedback `list` operation. */ +type MessageFeedbackListResult = + | MessageFeedbackSuccess + | MessageFeedbackRejected +``` + +```ts type-equiv +/** Result returned by the message-feedback `put` operation. */ +type MessageFeedbackPutResult = + | MessageFeedbackSuccess + | MessageFeedbackRejected< + | MessageFeedbackSessionNotFound + | MessageFeedbackTargetNotFound + | MessageFeedbackVersionConflict + | MessageFeedbackNoteBlank + | MessageFeedbackNoteTooLarge + > +``` + +```ts type-equiv +/** Result returned by the message-feedback `delete` operation. */ +type MessageFeedbackDeleteResult = + | MessageFeedbackSuccess + | MessageFeedbackRejected +``` + ## 数据与并发 每个 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 或审计身份,因此假设调用方边界可信。 diff --git a/packages/feedback/message-feedback/README.i18n.yaml b/packages/feedback/message-feedback/README.i18n.yaml index 97cd37d0b4..edf8ec947b 100644 --- a/packages/feedback/message-feedback/README.i18n.yaml +++ b/packages/feedback/message-feedback/README.i18n.yaml @@ -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 packages/feedback/message-feedback/README.md -README.md: 948cdcd2ef0fa2d1bcea68fdc40f16be08e963a8 -README.zh.md: 14e62eea0a182bc60be2c370653d1cb031d6b8a8 +README.md: a9ebad25907e32435c8d2a65eb4b2cd9eaa89eeb +README.zh.md: 29cbee0c1ec2ee810948d895c762d2e6320e9b66 diff --git a/packages/feedback/message-feedback/README.md b/packages/feedback/message-feedback/README.md index 948cdcd2ef..a9ebad2590 100644 --- a/packages/feedback/message-feedback/README.md +++ b/packages/feedback/message-feedback/README.md @@ -12,7 +12,7 @@ Public request, value, version, and failure types are exported from the package |---|---| | `maxNoteBytes` | Required positive safe integer: maximum UTF-8 byte length of one optional note. | -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 an authorized material `put` clears an existing note. +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. ```yaml - id: message-feedback @@ -55,6 +55,8 @@ A matching-version no-op returns the already stored item with unchanged version 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. +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. + ## Model Experience ### Local message-feedback state @@ -79,3 +81,4 @@ Independent. Listing or mutating message feedback does not touch a model request - **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. - **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. - **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. +- **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. diff --git a/packages/feedback/message-feedback/README.zh.md b/packages/feedback/message-feedback/README.zh.md index 14e62eea0a..29cbee0c1e 100644 --- a/packages/feedback/message-feedback/README.zh.md +++ b/packages/feedback/message-feedback/README.zh.md @@ -12,7 +12,7 @@ |---|---| | `maxNoteBytes` | 必填正 safe integer:一条可选备注的最大 UTF-8 字节长度。 | -备注必须包含至少一个非空白字符,但通过校验的文本按原样存储,不会 trim。省略 `note` 表示目标值不含备注,因此通过授权的实质 `put` 会清除已有备注。 +备注必须包含至少一个非空白字符,但通过校验的文本按原样存储,不会 trim。省略 `note` 表示目标值不含备注,因此 version 匹配的实质 `put` 会清除已有备注。备注校验早于 Session 查找,因此即使 Session 不存在,也可能在不访问持久化的情况下返回 `note-blank` 或 `note-too-large`。 ```yaml - id: message-feedback @@ -55,6 +55,8 @@ message feedback 不是 Session 日志内容或 Session 投影。它不发出 `f 按 Session 划分的 promise 队列覆盖检查、持久性校验、伴随记录读取、比较与整行写入。这些语义会串行化经由同一服务实例的并发变更;storage-domain 自身没有跨进程条件写。 +Plugin disposal 会先关闭变更接纳,排空已进入各个 Session 队列的所有操作,然后才关闭 storage domain。disposal 开始后提交的变更会以生命周期故障拒绝,不会进入正在关闭的 domain。 + ## 模型体验 ### 本地消息反馈状态 @@ -79,3 +81,4 @@ message feedback 不是 Session 日志内容或 Session 投影。它不发出 `f - **Detach/catalog retirement 窗口**——请求若恰好落在 live detach 之后、persistence catalog 物化 header 之前的极短窗口,可能收到 `session-not-found`;调用方应在 retirement materialization 后重试。 - **Header 身份不是内容指纹**——只有 `{createdAt, cwd}` 不同时才能识别复用;本契约无法区分保留相同 header 身份的克隆日志。 - **调用方边界受信任**——`list`/`put`/`delete` 不携带已认证的 actor 或审计身份。在加入授权与归属信息前,部署方必须只通过受信任或另行认证的边界暴露 Host gateway。 +- **目录与行边界**——由于 persistence 没有按 id 读取元数据的操作,cold 请求会扫描完整的 Session snapshot 目录。`maxNoteBytes` 只限制单条备注,单个 Session 行的条目数和聚合保留字节尚无上限;按索引读取元数据和由部署决定的行边界,延后到具体消费方明确策略时处理。 diff --git a/packages/feedback/message-feedback/src/index.ts b/packages/feedback/message-feedback/src/index.ts index 6468100cc5..6126e837e1 100644 --- a/packages/feedback/message-feedback/src/index.ts +++ b/packages/feedback/message-feedback/src/index.ts @@ -158,6 +158,7 @@ export class MessageFeedbackService extends GatewayService { private readonly maxNoteBytes: number private table?: KvTable private readonly operationTails = new Map>() + private mutationAdmissionOpen = true /** * @param ctx - Host context carrying persistence and the storage-domain form. @@ -171,7 +172,11 @@ export class MessageFeedbackService extends GatewayService { /** Open and own the one message-feedback sidecar domain. */ protected async [Service.init](): Promise { const domain = await this.ctx.storageDomain.open(messageFeedbackDomainSpec) - this.ctx.effect(() => () => domain.close(), 'message-feedback.domainClose') + this.ctx.effect(() => async () => { + this.mutationAdmissionOpen = false + await Promise.all(this.operationTails.values()) + await domain.close() + }, 'message-feedback.domainClose') this.table = domain.table('sessions') } @@ -354,6 +359,9 @@ export class MessageFeedbackService extends GatewayService { /** Queue a complete read/compare/write mutation behind this Session's prior mutation. */ private enqueue(sessionId: SessionId, operation: () => Promise): Promise { + if (!this.mutationAdmissionOpen) { + return Promise.reject(new Error('message-feedback: service is disposing')) + } const previous = this.operationTails.get(sessionId) ?? Promise.resolve() const result = previous.then(operation) const tail = result.then(() => undefined, () => undefined) diff --git a/packages/feedback/message-feedback/src/spec.ts b/packages/feedback/message-feedback/src/spec.ts index 5dcefd9cfe..d08a536d06 100644 --- a/packages/feedback/message-feedback/src/spec.ts +++ b/packages/feedback/message-feedback/src/spec.ts @@ -22,6 +22,8 @@ export const messageFeedbackVersionSchema = z.uuid() .transform(value => value as MessageFeedbackVersion) /** Runtime schema for one current feedback item. */ +// Zod infers transformed branded fields structurally, so it cannot name the +// public interface even though every branded output is created below. export const messageFeedbackItemSchema = z.object({ messageId: z.string().min(1).transform(value => value as MessageId), rating: messageFeedbackRatingSchema, diff --git a/packages/feedback/message-feedback/tests/helpers.ts b/packages/feedback/message-feedback/tests/helpers.ts index c0c327500b..1dfaa24396 100644 --- a/packages/feedback/message-feedback/tests/helpers.ts +++ b/packages/feedback/message-feedback/tests/helpers.ts @@ -116,7 +116,7 @@ class TestPersistence extends SessionPersistence { inspectFailure: Error | undefined inspectCalls = 0 readFromCalls = 0 - onReadFrom: (() => void) | undefined + onReadFrom: (() => void | Promise) | undefined onListSnapshots: (() => void | Promise) | undefined locate(_meta: SessionHeader): SessionLocation | undefined { return undefined } @@ -140,16 +140,16 @@ class TestPersistence extends SessionPersistence { : Promise.resolve(stored) } - readFrom( + async readFrom( id: SessionId, fromSeq: number, ): Promise<{ meta: SessionHeader; events: SessionEvent[] }> { this.readFromCalls += 1 - this.onReadFrom?.() + await this.onReadFrom?.() const stored = this.durable.get(id) return stored === undefined ? Promise.reject(new Error(`test persistence: session '${id}' not found`)) - : Promise.resolve({ meta: stored.meta, events: stored.events.filter(event => event.seq >= fromSeq) }) + : { meta: stored.meta, events: stored.events.filter(event => event.seq >= fromSeq) } } list(): Promise { @@ -177,6 +177,7 @@ export interface TestHarness { readonly ctx: Context readonly persistence: TestPersistence readonly root: string + disposeFeedback(): Promise dispose(): Promise } @@ -184,22 +185,26 @@ export interface TestHarness { export async function setupHarness(maxNoteBytes = 64): Promise { const root = await mkdtemp(join(tmpdir(), 'dsh-message-feedback-test-')) const ctx = new Context() + let disposeFeedback: (() => Promise) | undefined try { await ctx.plugin(SessionStore) await ctx.plugin(TestPersistence) await ctx.plugin(Storage) await ctx.plugin(StorageJson, { root }) await ctx.plugin(StorageDomain, { backend: 'json' }) - await ctx.plugin(MessageFeedbackService, { maxNoteBytes }) + const feedbackFiber = await ctx.plugin(MessageFeedbackService, { maxNoteBytes }) + disposeFeedback = feedbackFiber.dispose } catch (error) { await ctx.fiber.dispose() await rm(root, { recursive: true, force: true }) throw error } + if (disposeFeedback === undefined) throw new Error('message feedback test plugin did not load') return { ctx, persistence: ctx.sessionPersistence as unknown as TestPersistence, root, + disposeFeedback, async dispose() { await ctx.fiber.dispose() await rm(root, { recursive: true, force: true }) diff --git a/packages/feedback/message-feedback/tests/invariant.spec.ts b/packages/feedback/message-feedback/tests/invariant.spec.ts new file mode 100644 index 0000000000..e88131e147 --- /dev/null +++ b/packages/feedback/message-feedback/tests/invariant.spec.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from 'vitest' +import InvariantService from '@deepseek-ai/dsh-invariants' +import * as MessageFeedbackInvariant from '../src/invariant.ts' +import { setupHarness } from './helpers.ts' + +describe('message-feedback invariant companion', () => { + it('removes its registry contribution when its fiber is disposed (HMR safety)', async () => { + const harness = await setupHarness() + try { + await harness.ctx.plugin(InvariantService) + const fiber = await harness.ctx.plugin(MessageFeedbackInvariant) + + expect(() => { + harness.ctx.invariants.register('@deepseek-ai/dsh-message-feedback', () => {}) + }).toThrow(/already registered/u) + + await fiber.dispose() + await expect(harness.ctx.plugin(MessageFeedbackInvariant).await()).resolves.toBeDefined() + } finally { + await harness.dispose() + } + }) +}) diff --git a/packages/feedback/message-feedback/tests/message-feedback.spec.ts b/packages/feedback/message-feedback/tests/message-feedback.spec.ts index d65000a682..f4478ec202 100644 --- a/packages/feedback/message-feedback/tests/message-feedback.spec.ts +++ b/packages/feedback/message-feedback/tests/message-feedback.spec.ts @@ -459,6 +459,57 @@ describe('MessageFeedbackService item concurrency', () => { })) expect(newItem.version).not.toBe(oldItem.version) }) + + it('drains admitted mutations before domain close and rejects later admission', async () => { + const current = await harness() + const { ctx, persistence } = current + const fixture = messageFixture('dispose-quiescence') + persistence.persist(fixture.session) + const service = ctx.messageFeedback + const lifecycle = service as unknown as { readonly mutationAdmissionOpen: boolean } + const started = Promise.withResolvers() + const release = Promise.withResolvers() + let physicalReads = 0 + let committed = 0 + persistence.onReadFrom = async () => { + physicalReads += 1 + if (physicalReads !== 1) return + started.resolve(undefined) + await release.promise + } + ctx.on('domain/changed', (change) => { + if (change.domain === 'message_feedback') committed += 1 + }) + + const first = service.put({ + sessionId: fixture.session.id, + messageId: fixture.assistantMessageIds[0], + rating: 'positive', + ifVersion: null, + }) + await started.promise + const second = service.put({ + sessionId: fixture.session.id, + messageId: fixture.assistantMessageIds[1], + rating: 'negative', + ifVersion: null, + }) + const disposal = current.disposeFeedback() + await vi.waitFor(() => { expect(lifecycle.mutationAdmissionOpen).toBe(false) }) + + await expect(service.delete({ + sessionId: fixture.session.id, + messageId: fixture.assistantMessageIds[0], + ifVersion: staleVersion(), + })).rejects.toThrow('message-feedback: service is disposing') + release.resolve(undefined) + + expectItem(await first) + expectItem(await second) + await disposal + expect(physicalReads).toBe(2) + expect(committed).toBe(2) + }) }) describe('MessageFeedbackService durability ordering', () => { diff --git a/scripts/type-equiv.manifest.json b/scripts/type-equiv.manifest.json index c1b1b2753a..3c6ef4e192 100644 --- a/scripts/type-equiv.manifest.json +++ b/scripts/type-equiv.manifest.json @@ -1739,6 +1739,101 @@ "doc": "docs/subsystems/core.md", "symbol": "AgentOptions", "source": "packages/core/agent/src/runtime-types.ts" + }, + { + "doc": "docs/subsystems/feedback.md", + "symbol": "MessageFeedbackVersion", + "source": "packages/feedback/message-feedback/src/types.ts" + }, + { + "doc": "docs/subsystems/feedback.md", + "symbol": "MessageFeedbackRating", + "source": "packages/feedback/message-feedback/src/types.ts" + }, + { + "doc": "docs/subsystems/feedback.md", + "symbol": "MessageFeedbackItem", + "source": "packages/feedback/message-feedback/src/types.ts" + }, + { + "doc": "docs/subsystems/feedback.md", + "symbol": "MessageFeedbackListRequest", + "source": "packages/feedback/message-feedback/src/types.ts" + }, + { + "doc": "docs/subsystems/feedback.md", + "symbol": "MessageFeedbackListValue", + "source": "packages/feedback/message-feedback/src/types.ts" + }, + { + "doc": "docs/subsystems/feedback.md", + "symbol": "MessageFeedbackPutRequest", + "source": "packages/feedback/message-feedback/src/types.ts" + }, + { + "doc": "docs/subsystems/feedback.md", + "symbol": "MessageFeedbackDeleteRequest", + "source": "packages/feedback/message-feedback/src/types.ts" + }, + { + "doc": "docs/subsystems/feedback.md", + "symbol": "MessageFeedbackDeleteValue", + "source": "packages/feedback/message-feedback/src/types.ts" + }, + { + "doc": "docs/subsystems/feedback.md", + "symbol": "MessageFeedbackSessionNotFound", + "source": "packages/feedback/message-feedback/src/types.ts" + }, + { + "doc": "docs/subsystems/feedback.md", + "symbol": "MessageFeedbackTargetNotFound", + "source": "packages/feedback/message-feedback/src/types.ts" + }, + { + "doc": "docs/subsystems/feedback.md", + "symbol": "MessageFeedbackVersionConflict", + "source": "packages/feedback/message-feedback/src/types.ts" + }, + { + "doc": "docs/subsystems/feedback.md", + "symbol": "MessageFeedbackNoteBlank", + "source": "packages/feedback/message-feedback/src/types.ts" + }, + { + "doc": "docs/subsystems/feedback.md", + "symbol": "MessageFeedbackNoteTooLarge", + "source": "packages/feedback/message-feedback/src/types.ts" + }, + { + "doc": "docs/subsystems/feedback.md", + "symbol": "MessageFeedbackFailure", + "source": "packages/feedback/message-feedback/src/types.ts" + }, + { + "doc": "docs/subsystems/feedback.md", + "symbol": "MessageFeedbackSuccess", + "source": "packages/feedback/message-feedback/src/types.ts" + }, + { + "doc": "docs/subsystems/feedback.md", + "symbol": "MessageFeedbackRejected", + "source": "packages/feedback/message-feedback/src/types.ts" + }, + { + "doc": "docs/subsystems/feedback.md", + "symbol": "MessageFeedbackListResult", + "source": "packages/feedback/message-feedback/src/types.ts" + }, + { + "doc": "docs/subsystems/feedback.md", + "symbol": "MessageFeedbackPutResult", + "source": "packages/feedback/message-feedback/src/types.ts" + }, + { + "doc": "docs/subsystems/feedback.md", + "symbol": "MessageFeedbackDeleteResult", + "source": "packages/feedback/message-feedback/src/types.ts" } ] } diff --git a/tsconfig.host.json b/tsconfig.host.json index c616cc16aa..9473e8b923 100644 --- a/tsconfig.host.json +++ b/tsconfig.host.json @@ -14,6 +14,7 @@ "apps/web/tests/support.ts", "apps/web/tests/scaffold-hermetic.e2e.ts", "apps/web/tests/minimal-preset.snapshot.ts", + "apps/web/tests/message-feedback-protocol.snapshot.ts", "apps/web/tests/live-interactions.e2e.ts", "apps/web/tests/question-composer.e2e.ts", "apps/web/tests/approval-composer.e2e.ts",