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;
|
# 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:
|
# 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
|
# 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.md: 780cbaa840fcac7bcfa799468bfd61f5b08715cb
|
||||||
2026-08-10-message-feedback-sidecar.zh.md: da176e13586bb93ebadcefc0afb44c21ce386e08
|
2026-08-10-message-feedback-sidecar.zh.md: 72ecc82717010f65d013418a45c3b38c6084aaf9
|
||||||
|
|||||||
@@ -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.
|
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.
|
`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.
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ Status: implemented
|
|||||||
|
|
||||||
每个消息条目都携带自己的 opaque version,以及 Host 分配的 `createdAt` 和 `updatedAt` 时间戳。`put` 只把调用方的 `ifVersion` 与目标条目比较,因此编辑一条消息不会使另一条消息失效。即使目标值已经相同,比较仍然严格执行,从而防止陈旧请求穿过 ABA 值循环;冲突会返回权威当前条目,调用方无需二次读取即可协调。携带匹配 version 的无变化请求会保留 version 与时间戳;实质更新保留 `createdAt`、替换 version,并保证 `updatedAt` 不倒退。删除已经不存在的条目也同样成功。version 是只能做相等比较的 token,不是调用方可以排序或自行合成的计数器。
|
每个消息条目都携带自己的 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 契约的薄消费者。
|
`maxNoteBytes` 是必填的部署选择,用于限制可选备注的 UTF-8 字节长度;Web Host bundle 将其显式设为 `8192`。该包通过 `GatewayService` 与 `@Remote` 直接发布 Host `messageFeedback.list`、`messageFeedback.put` 与 `messageFeedback.delete` 契约。客户端 Remote 聚合挂载与 UI 由各自边界负责并保持延后;后续适配层只是该 Host 契约的薄消费者。
|
||||||
|
|
||||||
|
|||||||
115
apps/web/tests/message-feedback-protocol.snapshot.ts
Normal file
115
apps/web/tests/message-feedback-protocol.snapshot.ts
Normal file
@@ -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<string, unknown> {
|
||||||
|
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<unknown> => {
|
||||||
|
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'])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -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"}}}
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
"tests/scaffold.ts",
|
"tests/scaffold.ts",
|
||||||
"tests/scaffold-hermetic.e2e.ts",
|
"tests/scaffold-hermetic.e2e.ts",
|
||||||
"tests/minimal-preset.snapshot.ts",
|
"tests/minimal-preset.snapshot.ts",
|
||||||
|
"tests/message-feedback-protocol.snapshot.ts",
|
||||||
"tests/live-interactions.e2e.ts",
|
"tests/live-interactions.e2e.ts",
|
||||||
"tests/question-composer.e2e.ts",
|
"tests/question-composer.e2e.ts",
|
||||||
"tests/approval-composer.e2e.ts",
|
"tests/approval-composer.e2e.ts",
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write docs/subsystems/feedback.md
|
# pnpm run verify-translation-pairing --write docs/subsystems/feedback.md
|
||||||
feedback.md: e5c99ee639403ec3da0cf5845ec00f5ea1a5817d
|
feedback.md: 76a29f7d6ba604fa07ed56429c9b066e22639671
|
||||||
feedback.zh.md: 407d111da59af1d9a5c99c0e3d06d70df9051a13
|
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)
|
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
|
## 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.
|
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.
|
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
|
## Boundaries and limitations
|
||||||
|
|
||||||
- The client Remote aggregate mount and UI consumer are separately owned and deferred.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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)
|
来源:[`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 只能用于相等比较,且只与目标消息比较;调用方不能排序或自行合成它。
|
每个 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 是方法级权威。
|
服务通过 `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 消费方由各自边界负责并保持延后。
|
- 客户端 Remote 聚合挂载与 UI 消费方由各自边界负责并保持延后。
|
||||||
- 变更队列仅在进程内生效。storage-domain 没有跨进程条件写,因此多个 Host 写入同一存储根目录时,不提供 compare-and-swap 或防止丢失更新的保证。
|
- 变更队列仅在进程内生效。storage-domain 没有跨进程条件写,因此多个 Host 写入同一存储根目录时,不提供 compare-and-swap 或防止丢失更新的保证。
|
||||||
- Session persistence 没有持久删除接口。服务不把 `session/disposed` 或 `host/session-removed` 当作删除,因此不伪造级联;在带外移除日志后,孤儿伴随记录可能继续存在。
|
- Session persistence 没有持久删除接口。服务不把 `session/disposed` 或 `host/session-removed` 当作删除,因此不伪造级联;在带外移除日志后,孤儿伴随记录可能继续存在。
|
||||||
- 请求若恰好落在 live detach 之后、persistence catalog 物化 header 之前的极短窗口,可能收到 `session-not-found`;调用方应在 retirement materialization 后重试。
|
- 请求若恰好落在 live detach 之后、persistence catalog 物化 header 之前的极短窗口,可能收到 `session-not-found`;调用方应在 retirement materialization 后重试。
|
||||||
|
- 由于 persistence 没有按 id 读取元数据的操作,cold 请求会扫描完整的 Session snapshot 目录。单个 Session 行也没有条目数或聚合字节上限;在具体消费方拥有行策略之前,`maxNoteBytes` 只限制每条备注。
|
||||||
- 只有 `{createdAt, cwd}` 不同时,header 身份才能识别复用的 id;本契约无法区分保留相同 header 身份的克隆日志。
|
- 只有 `{createdAt, cwd}` 不同时,header 身份才能识别复用的 id;本契约无法区分保留相同 header 身份的克隆日志。
|
||||||
- Host 契约不记录已认证的 actor 或审计身份,因此假设调用方边界可信。
|
- Host 契约不记录已认证的 actor 或审计身份,因此假设调用方边界可信。
|
||||||
|
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write packages/feedback/message-feedback/README.md
|
# pnpm run verify-translation-pairing --write packages/feedback/message-feedback/README.md
|
||||||
README.md: 948cdcd2ef0fa2d1bcea68fdc40f16be08e963a8
|
README.md: a9ebad25907e32435c8d2a65eb4b2cd9eaa89eeb
|
||||||
README.zh.md: 14e62eea0a182bc60be2c370653d1cb031d6b8a8
|
README.zh.md: 29cbee0c1ec2ee810948d895c762d2e6320e9b66
|
||||||
|
|||||||
@@ -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. |
|
| `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
|
```yaml
|
||||||
- id: message-feedback
|
- 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.
|
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
|
## Model Experience
|
||||||
|
|
||||||
### Local message-feedback state
|
### 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.
|
- **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.
|
- **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.
|
- **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.
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `maxNoteBytes` | 必填正 safe integer:一条可选备注的最大 UTF-8 字节长度。 |
|
| `maxNoteBytes` | 必填正 safe integer:一条可选备注的最大 UTF-8 字节长度。 |
|
||||||
|
|
||||||
备注必须包含至少一个非空白字符,但通过校验的文本按原样存储,不会 trim。省略 `note` 表示目标值不含备注,因此通过授权的实质 `put` 会清除已有备注。
|
备注必须包含至少一个非空白字符,但通过校验的文本按原样存储,不会 trim。省略 `note` 表示目标值不含备注,因此 version 匹配的实质 `put` 会清除已有备注。备注校验早于 Session 查找,因此即使 Session 不存在,也可能在不访问持久化的情况下返回 `note-blank` 或 `note-too-large`。
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- id: message-feedback
|
- id: message-feedback
|
||||||
@@ -55,6 +55,8 @@ message feedback 不是 Session 日志内容或 Session 投影。它不发出 `f
|
|||||||
|
|
||||||
按 Session 划分的 promise 队列覆盖检查、持久性校验、伴随记录读取、比较与整行写入。这些语义会串行化经由同一服务实例的并发变更;storage-domain 自身没有跨进程条件写。
|
按 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 后重试。
|
- **Detach/catalog retirement 窗口**——请求若恰好落在 live detach 之后、persistence catalog 物化 header 之前的极短窗口,可能收到 `session-not-found`;调用方应在 retirement materialization 后重试。
|
||||||
- **Header 身份不是内容指纹**——只有 `{createdAt, cwd}` 不同时才能识别复用;本契约无法区分保留相同 header 身份的克隆日志。
|
- **Header 身份不是内容指纹**——只有 `{createdAt, cwd}` 不同时才能识别复用;本契约无法区分保留相同 header 身份的克隆日志。
|
||||||
- **调用方边界受信任**——`list`/`put`/`delete` 不携带已认证的 actor 或审计身份。在加入授权与归属信息前,部署方必须只通过受信任或另行认证的边界暴露 Host gateway。
|
- **调用方边界受信任**——`list`/`put`/`delete` 不携带已认证的 actor 或审计身份。在加入授权与归属信息前,部署方必须只通过受信任或另行认证的边界暴露 Host gateway。
|
||||||
|
- **目录与行边界**——由于 persistence 没有按 id 读取元数据的操作,cold 请求会扫描完整的 Session snapshot 目录。`maxNoteBytes` 只限制单条备注,单个 Session 行的条目数和聚合保留字节尚无上限;按索引读取元数据和由部署决定的行边界,延后到具体消费方明确策略时处理。
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ export class MessageFeedbackService extends GatewayService {
|
|||||||
private readonly maxNoteBytes: number
|
private readonly maxNoteBytes: number
|
||||||
private table?: KvTable<SessionId, MessageFeedbackRow>
|
private table?: KvTable<SessionId, MessageFeedbackRow>
|
||||||
private readonly operationTails = new Map<SessionId, Promise<void>>()
|
private readonly operationTails = new Map<SessionId, Promise<void>>()
|
||||||
|
private mutationAdmissionOpen = true
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param ctx - Host context carrying persistence and the storage-domain form.
|
* @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. */
|
/** Open and own the one message-feedback sidecar domain. */
|
||||||
protected async [Service.init](): Promise<void> {
|
protected async [Service.init](): Promise<void> {
|
||||||
const domain = await this.ctx.storageDomain.open(messageFeedbackDomainSpec)
|
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')
|
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. */
|
/** Queue a complete read/compare/write mutation behind this Session's prior mutation. */
|
||||||
private enqueue<T>(sessionId: SessionId, operation: () => Promise<T>): Promise<T> {
|
private enqueue<T>(sessionId: SessionId, operation: () => Promise<T>): Promise<T> {
|
||||||
|
if (!this.mutationAdmissionOpen) {
|
||||||
|
return Promise.reject(new Error('message-feedback: service is disposing'))
|
||||||
|
}
|
||||||
const previous = this.operationTails.get(sessionId) ?? Promise.resolve()
|
const previous = this.operationTails.get(sessionId) ?? Promise.resolve()
|
||||||
const result = previous.then(operation)
|
const result = previous.then(operation)
|
||||||
const tail = result.then(() => undefined, () => undefined)
|
const tail = result.then(() => undefined, () => undefined)
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ export const messageFeedbackVersionSchema = z.uuid()
|
|||||||
.transform(value => value as MessageFeedbackVersion)
|
.transform(value => value as MessageFeedbackVersion)
|
||||||
|
|
||||||
/** Runtime schema for one current feedback item. */
|
/** 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({
|
export const messageFeedbackItemSchema = z.object({
|
||||||
messageId: z.string().min(1).transform(value => value as MessageId),
|
messageId: z.string().min(1).transform(value => value as MessageId),
|
||||||
rating: messageFeedbackRatingSchema,
|
rating: messageFeedbackRatingSchema,
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ class TestPersistence extends SessionPersistence {
|
|||||||
inspectFailure: Error | undefined
|
inspectFailure: Error | undefined
|
||||||
inspectCalls = 0
|
inspectCalls = 0
|
||||||
readFromCalls = 0
|
readFromCalls = 0
|
||||||
onReadFrom: (() => void) | undefined
|
onReadFrom: (() => void | Promise<void>) | undefined
|
||||||
onListSnapshots: (() => void | Promise<void>) | undefined
|
onListSnapshots: (() => void | Promise<void>) | undefined
|
||||||
|
|
||||||
locate(_meta: SessionHeader): SessionLocation | undefined { return undefined }
|
locate(_meta: SessionHeader): SessionLocation | undefined { return undefined }
|
||||||
@@ -140,16 +140,16 @@ class TestPersistence extends SessionPersistence {
|
|||||||
: Promise.resolve(stored)
|
: Promise.resolve(stored)
|
||||||
}
|
}
|
||||||
|
|
||||||
readFrom(
|
async readFrom(
|
||||||
id: SessionId,
|
id: SessionId,
|
||||||
fromSeq: number,
|
fromSeq: number,
|
||||||
): Promise<{ meta: SessionHeader; events: SessionEvent[] }> {
|
): Promise<{ meta: SessionHeader; events: SessionEvent[] }> {
|
||||||
this.readFromCalls += 1
|
this.readFromCalls += 1
|
||||||
this.onReadFrom?.()
|
await this.onReadFrom?.()
|
||||||
const stored = this.durable.get(id)
|
const stored = this.durable.get(id)
|
||||||
return stored === undefined
|
return stored === undefined
|
||||||
? Promise.reject(new Error(`test persistence: session '${id}' not found`))
|
? 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<SessionHeader[]> {
|
list(): Promise<SessionHeader[]> {
|
||||||
@@ -177,6 +177,7 @@ export interface TestHarness {
|
|||||||
readonly ctx: Context
|
readonly ctx: Context
|
||||||
readonly persistence: TestPersistence
|
readonly persistence: TestPersistence
|
||||||
readonly root: string
|
readonly root: string
|
||||||
|
disposeFeedback(): Promise<void>
|
||||||
dispose(): Promise<void>
|
dispose(): Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,22 +185,26 @@ export interface TestHarness {
|
|||||||
export async function setupHarness(maxNoteBytes = 64): Promise<TestHarness> {
|
export async function setupHarness(maxNoteBytes = 64): Promise<TestHarness> {
|
||||||
const root = await mkdtemp(join(tmpdir(), 'dsh-message-feedback-test-'))
|
const root = await mkdtemp(join(tmpdir(), 'dsh-message-feedback-test-'))
|
||||||
const ctx = new Context()
|
const ctx = new Context()
|
||||||
|
let disposeFeedback: (() => Promise<void>) | undefined
|
||||||
try {
|
try {
|
||||||
await ctx.plugin(SessionStore)
|
await ctx.plugin(SessionStore)
|
||||||
await ctx.plugin(TestPersistence)
|
await ctx.plugin(TestPersistence)
|
||||||
await ctx.plugin(Storage)
|
await ctx.plugin(Storage)
|
||||||
await ctx.plugin(StorageJson, { root })
|
await ctx.plugin(StorageJson, { root })
|
||||||
await ctx.plugin(StorageDomain, { backend: 'json' })
|
await ctx.plugin(StorageDomain, { backend: 'json' })
|
||||||
await ctx.plugin(MessageFeedbackService, { maxNoteBytes })
|
const feedbackFiber = await ctx.plugin(MessageFeedbackService, { maxNoteBytes })
|
||||||
|
disposeFeedback = feedbackFiber.dispose
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await ctx.fiber.dispose()
|
await ctx.fiber.dispose()
|
||||||
await rm(root, { recursive: true, force: true })
|
await rm(root, { recursive: true, force: true })
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
if (disposeFeedback === undefined) throw new Error('message feedback test plugin did not load')
|
||||||
return {
|
return {
|
||||||
ctx,
|
ctx,
|
||||||
persistence: ctx.sessionPersistence as unknown as TestPersistence,
|
persistence: ctx.sessionPersistence as unknown as TestPersistence,
|
||||||
root,
|
root,
|
||||||
|
disposeFeedback,
|
||||||
async dispose() {
|
async dispose() {
|
||||||
await ctx.fiber.dispose()
|
await ctx.fiber.dispose()
|
||||||
await rm(root, { recursive: true, force: true })
|
await rm(root, { recursive: true, force: true })
|
||||||
|
|||||||
23
packages/feedback/message-feedback/tests/invariant.spec.ts
Normal file
23
packages/feedback/message-feedback/tests/invariant.spec.ts
Normal file
@@ -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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -459,6 +459,57 @@ describe('MessageFeedbackService item concurrency', () => {
|
|||||||
}))
|
}))
|
||||||
expect(newItem.version).not.toBe(oldItem.version)
|
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<undefined>()
|
||||||
|
const release = Promise.withResolvers<undefined>()
|
||||||
|
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', () => {
|
describe('MessageFeedbackService durability ordering', () => {
|
||||||
|
|||||||
@@ -1739,6 +1739,101 @@
|
|||||||
"doc": "docs/subsystems/core.md",
|
"doc": "docs/subsystems/core.md",
|
||||||
"symbol": "AgentOptions",
|
"symbol": "AgentOptions",
|
||||||
"source": "packages/core/agent/src/runtime-types.ts"
|
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"apps/web/tests/support.ts",
|
"apps/web/tests/support.ts",
|
||||||
"apps/web/tests/scaffold-hermetic.e2e.ts",
|
"apps/web/tests/scaffold-hermetic.e2e.ts",
|
||||||
"apps/web/tests/minimal-preset.snapshot.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/live-interactions.e2e.ts",
|
||||||
"apps/web/tests/question-composer.e2e.ts",
|
"apps/web/tests/question-composer.e2e.ts",
|
||||||
"apps/web/tests/approval-composer.e2e.ts",
|
"apps/web/tests/approval-composer.e2e.ts",
|
||||||
|
|||||||
Reference in New Issue
Block a user