{"_id":"@buckeyestudio/toh-message-feedback","name":"@buckeyestudio/toh-message-feedback","dist-tags":{"next":"0.1.1-rc.2","latest":"0.1.1-rc.2"},"versions":{"0.1.1-rc.2":{"name":"@buckeyestudio/toh-message-feedback","description":"Lifecycle-bound per-message rating and note sidecar for the TheOpen Harness","version":"0.1.1-rc.2","publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/dustinwloring1988/theopen-harness.git","directory":"packages/feedback/message-feedback"},"type":"module","main":"lib/index.js","types":"lib/types/index.d.ts","exports":{".":{"types":"./lib/types/index.d.ts","default":"./lib/index.js"},"./invariant":{"types":"./lib/types/invariant.d.ts","default":"./lib/invariant.js"},"./types":{"types":"./lib/types/types.d.ts","default":"./lib/types/types.js"},"./typert":{"types":"./lib/typert.host.d.ts","default":"./lib/typert.host.js"},"./remote":{"types":"./lib/typert.remote-client.d.ts","default":"./lib/typert.remote-client.js"},"./src/*":"./src/*","./package.json":"./package.json"},"license":"MIT","author":{"name":"buckeyestudio"},"peerDependencies":{"@buckeyestudio/toh-invariants":"^0.1.1-rc.2","@buckeyestudio/toh-brand":"^0.1.1-rc.2","@buckeyestudio/toh-llm":"^0.1.1-rc.2","@buckeyestudio/toh-session":"^0.1.1-rc.2","@buckeyestudio/toh-session-persistence":"^0.1.1-rc.2","@buckeyestudio/toh-storage-domain":"^0.1.1-rc.2","@buckeyestudio/toh-typert-protocol":"^0.1.1-rc.2","@buckeyestudio/cordis":"^4.0.1"},"dependencies":{"zod":"^4.4.3","@buckeyestudio/schemastery":"^3.18.1"},"devDependencies":{"@buckeyestudio/cordis-plugin-loader":"^1.0.2","@buckeyestudio/toh-brand":"^0.1.1-rc.2","@buckeyestudio/cordis-plugin-include":"^1.0.6","@buckeyestudio/toh-invariants":"^0.1.1-rc.2","@buckeyestudio/toh-llm":"^0.1.1-rc.2","@buckeyestudio/toh-session":"^0.1.1-rc.2","@buckeyestudio/toh-session-persistence":"^0.1.1-rc.2","@buckeyestudio/toh-session-persistence-jsonl":"^0.1.1-rc.2","@buckeyestudio/toh-storage-domain":"^0.1.1-rc.2","@buckeyestudio/toh-storage-json":"^0.1.1-rc.2","@buckeyestudio/toh-typert-protocol":"^0.1.1-rc.2","@buckeyestudio/toh-storage":"^0.1.1-rc.2","@buckeyestudio/cordis":"^4.0.1"},"_id":"@buckeyestudio/toh-message-feedback@0.1.1-rc.2","bugs":{"url":"https://github.com/dustinwloring1988/theopen-harness/issues"},"homepage":"https://github.com/dustinwloring1988/theopen-harness#readme","_integrity":"sha512-uv4QEhQFbIgFhV0Jzhi3g//4gJmMa0lzq+c7P+/lAky+IbuKRfT6e2hY+SdjCQpM2L2EeNtpEeK1zagL9xI1rQ==","_resolved":"/home/runner/work/theopen-harness/theopen-harness/dist/npm/buckeyestudio-toh-message-feedback-0.1.1-rc.2.tgz","_from":"file:/home/runner/work/theopen-harness/theopen-harness/dist/npm/buckeyestudio-toh-message-feedback-0.1.1-rc.2.tgz","_nodeVersion":"24.19.0","_npmVersion":"11.17.0","dist":{"integrity":"sha512-uv4QEhQFbIgFhV0Jzhi3g//4gJmMa0lzq+c7P+/lAky+IbuKRfT6e2hY+SdjCQpM2L2EeNtpEeK1zagL9xI1rQ==","shasum":"23b657a3bfa7a439e76a22de5a304723b45e19e4","tarball":"https://registry.npmjs.org/@buckeyestudio/toh-message-feedback/-/toh-message-feedback-0.1.1-rc.2.tgz","fileCount":19,"unpackedSize":98839,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCuZM6uguj2EkPFwpk+soS3Jnf/lss4+CSCgXi1EVs2AwIhAJeYw0Njjo8x34FvUrejAzDD79phw4lbrlypMLoCIyQh"}]},"_npmUser":{"name":"buckeyestudio","email":"dustinwloring1988@gmail.com"},"directories":{},"maintainers":[{"name":"buckeyestudio","email":"dustinwloring1988@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/toh-message-feedback_0.1.1-rc.2_1787489064243_0.9674775687082033"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-23T12:44:24.134Z","0.1.1-rc.2":"2026-08-23T12:44:24.394Z","modified":"2026-08-23T12:44:24.551Z"},"maintainers":[{"name":"buckeyestudio","email":"dustinwloring1988@gmail.com"}],"description":"Lifecycle-bound per-message rating and note sidecar for the TheOpen Harness","homepage":"https://github.com/dustinwloring1988/theopen-harness#readme","repository":{"type":"git","url":"git+https://github.com/dustinwloring1988/theopen-harness.git","directory":"packages/feedback/message-feedback"},"author":{"name":"buckeyestudio"},"bugs":{"url":"https://github.com/dustinwloring1988/theopen-harness/issues"},"license":"MIT","readme":"# @buckeyestudio/toh-message-feedback\n\n[English](README.md) | 中文\n\n本包提供由 Host 拥有、针对单条已完成 assistant 消息的可编辑反馈。它注册 `ctx.messageFeedback`，在 storage-domain 中为每个 Session 持久化一条绑定生命周期的伴随记录（sidecar），并发布 Host `messageFeedback.list`、`messageFeedback.put` 与 `messageFeedback.delete` 一元 Remote 契约。它与不可变的 Session 级 `feedback/record` 事件相互独立，不执行遥测交接。[消息反馈伴随记录 Agent Note](../../../.agents/notes/implemented/architecture/2026-08-10-message-feedback-sidecar.zh.md)拥有其设计边界。\n\n公开的请求、值、版本与失败类型从包根入口及 `@buckeyestudio/toh-message-feedback/types` 导出；其源码为 [`src/types.ts`](src/types.ts)。\n\n## 配置\n\n| 键 | 含义 |\n|---|---|\n| `maxNoteBytes` | 必填正 safe integer：一条可选备注的最大 UTF-8 字节长度。 |\n\n备注必须包含至少一个非空白字符，但通过校验的文本按原样存储，不会 trim。省略 `note` 表示目标值不含备注，因此 version 匹配的实质 `put` 会清除已有备注。备注校验早于 Session 查找，因此即使 Session 不存在，也可能在不访问持久化的情况下返回 `note-blank` 或 `note-too-large`。\n\n```yaml\n- id: message-feedback\n  name: '@buckeyestudio/toh-message-feedback'\n  config:\n    maxNoteBytes: 8192\n```\n\n服务注入 `storageDomain`、`sessionPersistence` 与 `sessions`。其持久存储域为 `message_feedback`，其中 `sessions` 表按 `SessionId` 每个一行。\n\n## 数据、生命周期与持久性\n\n`MessageFeedbackItem` 包含 `messageId`、`rating: 'positive' | 'negative'`、可选 `note`、只能做相等比较的 opaque `version`，以及由 Host 分配、以 Unix 毫秒表示的 `createdAt`/`updatedAt` 时间戳。实质更新保留 `createdAt`、替换 `version`，并保证 `updatedAt` 不倒退。`list` 按首次创建顺序返回新的不可变快照；更新条目时保留其位置，删除后再创建则追加为新条目。\n\n每条存储行都携带检查所得 Session header 身份 `{createdAt, cwd}`。不匹配按不存在处理：`list` 返回空 `items` 数组，`delete` 返回已不存在的后置条件，`put` 可以用绑定当前身份的新行替换陈旧行。这会在复用的 `SessionId` 具有不同 header 身份时形成隔离。fork 使用独立的 Session 身份，不复制反馈伴随记录。\n\n`SessionPersistence.inspect()` 提供 cold-safe 观测，不发布或恢复 Agent，也不提交 cold repair。对于没有 live owner 的 Session，系统先用 `listSnapshots()` 判定明确不存在；已进入目录的 Session 若 `inspect()` 失败，仍属于基础设施故障，不会被猜测成 `session-not-found`。`put` 只接受具有指定 `MessageId` 的非空、append-origin `assistant/message`；replacement-origin 消息、仅承载 usage 的空 assistant 记录与非 assistant 记录都返回 `target-not-found`。\n\n初步校验后，`put` 在写入伴随记录前建立 durability barrier。身份匹配的 live Session 先通过权威 `ctx.sessions.flush` checkpoint 提交，随后 live 与 cold 路径都会通过 `SessionPersistence.readFrom` 从序列零做物理复读。之后再次校验所得观测的 header 身份与目标。缺少 flush 参与方、身份变化、目标消失或物理读取失败都会阻止伴随记录提交，因此持久反馈绝不会先于其持久目标消息。\n\nmessage feedback 不是 Session 日志内容或 Session 投影。它不发出 `feedback/record` 事件，不进入模型历史，也不触发 `FEEDBACK_ONLY` 遥测释放。\n\n## 服务与 Host Remote 契约\n\n`TypertRemoteService` 与 `@Remote` 将 `MessageFeedbackService` 的同三个方法发布出去；Host endpoint 名称为 `messageFeedback.list`、`messageFeedback.put` 与 `messageFeedback.delete`。每个方法都返回判别式业务 union：`{ ok: true, value }` 或 `{ ok: false, error }`。存储、损坏或缺少 durability listener 等操作故障会产生 reject，不会被误标为业务错误。\n\n| 方法 | 请求 | 成功 `value` | 拒绝的 `error.code` |\n|---|---|---|---|\n| `list` | `MessageFeedbackListRequest { sessionId }` | `MessageFeedbackListValue { items }` | `session-not-found` |\n| `put` | `MessageFeedbackPutRequest { sessionId, messageId, rating, note?, ifVersion }` | 已提交的 `MessageFeedbackItem` | `session-not-found`、`target-not-found`、`version-conflict`、`note-blank`、`note-too-large` |\n| `delete` | `MessageFeedbackDeleteRequest { sessionId, messageId, ifVersion }` | `MessageFeedbackDeleteValue { absent: true }` | `session-not-found`、`version-conflict` |\n\n`MessageFeedbackVersionConflict` 返回权威 `current` 条目；条目不存在时为 `null`。调用方无需额外执行 `list`，即可协调当前 rating、note 与 version。`MessageFeedbackNoteTooLarge` 同时返回 `maxBytes` 与 `actualBytes`。客户端 Remote 聚合尚未挂载生成的客户端 contribution；Host 调用方无需该客户端组装即可使用 service/Remote 契约。\n\n## Compare-and-set 与幂等性\n\n`ifVersion: null` 表示仅当条目不存在时才创建；已有条目的每次请求都必须与其当前 version 完全一致，即使目标值已经相同、不会产生实质更新。检查按消息而非按 Session 进行，因此修改一个条目不会与另一个条目冲突。每次实质创建或更新都会分配新的 opaque UUID token，防止陈旧写入穿过 ABA 值循环。\n\n携带匹配 version 的无变化请求会返回已存条目，version 与时间戳均不变。成功响应丢失后，使用旧 token 重试会得到 `version-conflict.current`；调用方无需额外读取，即可把权威当前值与目标值比较。条目已不存在时，`delete` 忽略 `ifVersion`；成功后始终返回稳定的 `{ absent: true }` 后置条件。\n\n按 Session 划分的 promise 队列覆盖检查、持久性校验、伴随记录读取、比较与整行写入。这些语义会串行化经由同一服务实例的并发变更；storage-domain 自身没有跨进程条件写。\n\nPlugin disposal 会先关闭变更接纳，排空已进入各个 Session 队列的所有操作，然后才关闭 storage domain。disposal 开始后提交的变更会以生命周期故障拒绝，不会进入正在关闭的 domain。\n\n## 模型体验\n\n### 本地消息反馈状态\n\n#### 模型看到的内容\n\n无。`ctx.messageFeedback` 不注册工具、提示词段落、模型可见上下文或 Session 事件；除非另一个具有独立文档的 Consumer 显式公开反馈，否则它只留在 Host 拥有的伴随记录中。\n\n#### Token 影响\n\n为零。本包的请求、结果、评分、备注、时间戳或失败都不会进入模型请求。\n\n#### KV Cache 影响\n\n相互独立。读取或变更消息反馈不会触碰模型请求前缀，也不会使本可复用的提供方缓存条目失效。\n\n## 已知局限与延后工作\n\n- **缺少客户端聚合与 UI**——Host Remote 契约已经发布，但客户端 Remote 聚合 contribution 与任何 UI 消费方由各自边界负责并保持延后。\n- **Compare-and-set 仅限单进程**——按 Session 划分的队列只串行化一个服务实例；storage-domain 不提供跨进程条件写，因此多个 Host 进程写入同一存储根目录时仍可能丢失更新。\n- **没有持久 Session 删除级联**——Session persistence 没有删除接口，且 `session/disposed`/`host/session-removed` 表示 detach 而非持久删除。因此服务会保留空行，并可能在带外移除日志后留下遗留行，而不会在 detach 时删除仍有效的反馈。\n- **Detach/catalog retirement 窗口**——请求若恰好落在 live detach 之后、persistence catalog 物化 header 之前的极短窗口，可能收到 `session-not-found`；调用方应在 retirement materialization 后重试。\n- **Header 身份不是内容指纹**——只有 `{createdAt, cwd}` 不同时才能识别复用；本契约无法区分保留相同 header 身份的克隆日志。\n- **调用方边界受信任**——`list`/`put`/`delete` 不携带已认证的 actor 或审计身份。在加入授权与归属信息前，部署方必须只通过受信任或另行认证的边界暴露 Host gateway。\n- **目录与行边界**——由于 persistence 没有按 id 读取元数据的操作，cold 请求会扫描完整的 Session snapshot 目录。`maxNoteBytes` 只限制单条备注，单个 Session 行的条目数和聚合保留字节尚无上限；按索引读取元数据和由部署决定的行边界，延后到具体消费方明确策略时处理。\n","readmeFilename":"README.zh.md","_rev":"1-0ac1a0230dbf594f9c676e842865146a"}