@deepseek-ai/dsh-message-feedback 0.0.1-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, DeepSeek
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,6 @@
1
+ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
+ # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
+ # after editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write packages/feedback/message-feedback/README.md
5
+ README.md: 54c6b92fbb68027a4aedb198948785183661a724
6
+ README.zh.md: 29cbee0c1ec2ee810948d895c762d2e6320e9b66
package/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # @deepseek-ai/dsh-message-feedback
2
+
3
+ English | [中文](README.zh.md)
4
+
5
+ Host-owned editable feedback for one finalized assistant message. The package registers `ctx.messageFeedback`, persists one lifecycle-bound sidecar row per Session in storage-domain, and publishes the Host `messageFeedback.list`, `messageFeedback.put`, and `messageFeedback.delete` unary Remote contract. It is separate from the immutable Session-level `feedback/record` event and performs no telemetry handoff. The [message-feedback sidecar Agent Note](../../../.agents/notes/implemented/architecture/2026-08-10-message-feedback-sidecar.md) owns the design boundary.
6
+
7
+ Public request, value, version, and failure types are exported from the package root and `@deepseek-ai/dsh-message-feedback/types`; [`src/types.ts`](src/types.ts) is their source.
8
+
9
+ ## Configuration
10
+
11
+ | key | meaning |
12
+ |---|---|
13
+ | `maxNoteBytes` | Required positive safe integer: maximum UTF-8 byte length of one optional note. |
14
+
15
+ Notes must contain at least one non-whitespace character, but accepted text is stored verbatim rather than trimmed. Omitting `note` means the desired value has no note, so a version-matched material `put` clears an existing note. Note validation precedes Session lookup and can therefore return `note-blank` or `note-too-large` for a missing Session without touching persistence.
16
+
17
+ ```yaml
18
+ - id: message-feedback
19
+ name: '@deepseek-ai/dsh-message-feedback'
20
+ config:
21
+ maxNoteBytes: 8192
22
+ ```
23
+
24
+ The service injects `storageDomain`, `sessionPersistence`, and `sessions`. Its durable domain is `message_feedback`, with one `sessions` table row per `SessionId`.
25
+
26
+ ## Data, lifecycle, and durability
27
+
28
+ `MessageFeedbackItem` contains `messageId`, `rating: 'positive' | 'negative'`, optional `note`, an opaque equality-only `version`, and Host-assigned `createdAt`/`updatedAt` Unix-millisecond timestamps. A material update preserves `createdAt`, replaces `version`, and keeps `updatedAt` from moving backward. `list` returns fresh immutable snapshots in first-creation order; updating an item retains its place, while deleting and later recreating it appends a new item.
29
+
30
+ Each stored row carries the inspected Session header identity `{createdAt, cwd}`. A mismatch is treated as absence: `list` returns an empty `items` array, `delete` returns the absent postcondition, and `put` may replace the stale row with one bound to the current identity. This fences a reused `SessionId` when its header identity differs. Forks use a distinct Session identity and receive no feedback-row copy.
31
+
32
+ `SessionPersistence.inspect()` supplies a cold-safe observation without publishing or resuming an Agent and without committing cold repair. For a Session without a live owner, `listSnapshots()` first decides definite absence; an `inspect()` failure for a catalogued Session remains an infrastructure failure rather than being guessed into `session-not-found`. `put` accepts only a non-empty, append-origin `assistant/message` with the requested `MessageId`; replacement-origin messages, empty usage-only assistant records, and non-assistant records return `target-not-found`.
33
+
34
+ After initial validation, `put` establishes a durability barrier before writing the sidecar. A matching live Session commits through the canonical `ctx.sessions.flush` checkpoint, then both live and cold paths are physically read from sequence zero through `SessionPersistence.readFrom`. The resulting observation's header identity and target are validated again. A missing flush participant, changed identity, vanished target, or physical-read failure prevents the sidecar commit, so durable feedback never precedes the durable target message.
35
+
36
+ Message feedback is not Session-log content or a Session projection. It emits no `feedback/record` event, does not enter model history, and does not trigger `FEEDBACK_ONLY` telemetry release.
37
+
38
+ ## Service and Host Remote contract
39
+
40
+ The same three `MessageFeedbackService` methods are published by `GatewayService` and `@Remote`; the Host endpoint names are `messageFeedback.list`, `messageFeedback.put`, and `messageFeedback.delete`. Every method returns a discriminated business union: `{ ok: true, value }` or `{ ok: false, error }`. Operational storage, corruption, or missing-durability-listener failures reject instead of being mislabeled as business errors.
41
+
42
+ | Method | Request | Success `value` | Rejected `error.code` |
43
+ |---|---|---|---|
44
+ | `list` | `MessageFeedbackListRequest { sessionId }` | `MessageFeedbackListValue { items }` | `session-not-found` |
45
+ | `put` | `MessageFeedbackPutRequest { sessionId, messageId, rating, note?, ifVersion }` | committed `MessageFeedbackItem` | `session-not-found`, `target-not-found`, `version-conflict`, `note-blank`, `note-too-large` |
46
+ | `delete` | `MessageFeedbackDeleteRequest { sessionId, messageId, ifVersion }` | `MessageFeedbackDeleteValue { absent: true }` | `session-not-found`, `version-conflict` |
47
+
48
+ `MessageFeedbackVersionConflict` returns the authoritative `current` item, or `null` when no item exists. This lets a caller reconcile the current rating, note, and version without a second `list` request. `MessageFeedbackNoteTooLarge` returns both `maxBytes` and `actualBytes`. The Client Remote aggregate does not mount the generated client contribution yet; Host callers can use the service/Remote contract without that client assembly.
49
+
50
+ ## Compare-and-set and idempotency
51
+
52
+ `ifVersion: null` requests creation only; every request for an existing item requires its exact current version, including a no-op whose desired value already matches. The check is per message rather than per Session, so changing one item does not conflict with another. Every material create or update assigns a fresh opaque UUID token, preventing stale writes from crossing an ABA value cycle.
53
+
54
+ A matching-version no-op returns the already stored item with unchanged version and timestamps. After a lost success response, a retry with the old token receives `version-conflict.current`; the caller can compare that authoritative item with its desired value without an extra read. `delete` ignores `ifVersion` when the item is already absent and always returns the stable `{ absent: true }` postcondition after success.
55
+
56
+ A per-Session promise queue encloses inspection, durability validation, sidecar read, comparison, and whole-row write. These semantics serialize concurrent mutations through one service instance; storage-domain itself has no cross-process conditional write.
57
+
58
+ Plugin disposal closes mutation admission, drains every operation already accepted into the per-Session queues, and only then closes the storage domain. A mutation submitted after disposal begins rejects as a lifecycle failure instead of entering a closing domain.
59
+
60
+ ## Model Experience
61
+
62
+ ### Local message-feedback state
63
+
64
+ #### What the model sees
65
+
66
+ Nothing. `ctx.messageFeedback` registers no tool, prompt section, model-facing context, or Session event; feedback stays in a Host-owned sidecar unless a separately documented Consumer explicitly exposes it.
67
+
68
+ #### Token effect
69
+
70
+ Zero. No request, result, rating, note, timestamp, or failure from this package enters a model request.
71
+
72
+ #### KV Cache effect
73
+
74
+ Independent. Listing or mutating message feedback does not touch a model request prefix and cannot invalidate an otherwise reusable provider cache entry.
75
+
76
+ ## Known Limitations and Deferred Work
77
+
78
+ - **Client aggregate and UI are absent** — the Host Remote contract ships, but the Client Remote aggregate contribution and any UI consumer are separately owned and deferred.
79
+ - **Compare-and-set is single-process** — the per-Session queue serializes one service instance only; multiple Host processes writing one storage root can still lose updates because storage-domain exposes no cross-process conditional write.
80
+ - **No durable Session deletion cascade** — Session persistence has no deletion API, and `session/disposed`/`host/session-removed` mean detach rather than durable deletion. The service therefore retains empty rows and may leave orphan rows after out-of-band log removal instead of deleting valid feedback on detach.
81
+ - **Detach/catalog retirement window** — a request in the narrow interval after live detach but before the persistence catalog materializes the header can receive `session-not-found`; callers retry after retirement materialization.
82
+ - **Header identity is not a content fingerprint** — `{createdAt, cwd}` detects reuse only when those fields differ; a cloned log retaining the same header identity is indistinguishable.
83
+ - **Trusted caller boundary** — `list`/`put`/`delete` carry no authenticated actor or audit identity. A deployment must expose the Host gateway only through its trusted or separately authenticated boundary until authorization and attribution are added.
84
+ - **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.
package/README.zh.md ADDED
@@ -0,0 +1,84 @@
1
+ # @deepseek-ai/dsh-message-feedback
2
+
3
+ [English](README.md) | 中文
4
+
5
+ 本包提供由 Host 拥有、针对单条已完成 assistant 消息的可编辑反馈。它注册 `ctx.messageFeedback`,在 storage-domain 中为每个 Session 持久化一条绑定生命周期的伴随记录(sidecar),并发布 Host `messageFeedback.list`、`messageFeedback.put` 与 `messageFeedback.delete` 一元 Remote 契约。它与不可变的 Session 级 `feedback/record` 事件相互独立,不执行遥测交接。[消息反馈伴随记录 Agent Note](../../../.agents/notes/implemented/architecture/2026-08-10-message-feedback-sidecar.md)拥有其设计边界。
6
+
7
+ 公开的请求、值、版本与失败类型从包根入口及 `@deepseek-ai/dsh-message-feedback/types` 导出;其源码为 [`src/types.ts`](src/types.ts)。
8
+
9
+ ## 配置
10
+
11
+ | 键 | 含义 |
12
+ |---|---|
13
+ | `maxNoteBytes` | 必填正 safe integer:一条可选备注的最大 UTF-8 字节长度。 |
14
+
15
+ 备注必须包含至少一个非空白字符,但通过校验的文本按原样存储,不会 trim。省略 `note` 表示目标值不含备注,因此 version 匹配的实质 `put` 会清除已有备注。备注校验早于 Session 查找,因此即使 Session 不存在,也可能在不访问持久化的情况下返回 `note-blank` 或 `note-too-large`。
16
+
17
+ ```yaml
18
+ - id: message-feedback
19
+ name: '@deepseek-ai/dsh-message-feedback'
20
+ config:
21
+ maxNoteBytes: 8192
22
+ ```
23
+
24
+ 服务注入 `storageDomain`、`sessionPersistence` 与 `sessions`。其持久存储域为 `message_feedback`,其中 `sessions` 表按 `SessionId` 每个一行。
25
+
26
+ ## 数据、生命周期与持久性
27
+
28
+ `MessageFeedbackItem` 包含 `messageId`、`rating: 'positive' | 'negative'`、可选 `note`、只能做相等比较的 opaque `version`,以及由 Host 分配、以 Unix 毫秒表示的 `createdAt`/`updatedAt` 时间戳。实质更新保留 `createdAt`、替换 `version`,并保证 `updatedAt` 不倒退。`list` 按首次创建顺序返回新的不可变快照;更新条目时保留其位置,删除后再创建则追加为新条目。
29
+
30
+ 每条存储行都携带检查所得 Session header 身份 `{createdAt, cwd}`。不匹配按不存在处理:`list` 返回空 `items` 数组,`delete` 返回已不存在的后置条件,`put` 可以用绑定当前身份的新行替换陈旧行。这会在复用的 `SessionId` 具有不同 header 身份时形成隔离。fork 使用独立的 Session 身份,不复制反馈伴随记录。
31
+
32
+ `SessionPersistence.inspect()` 提供 cold-safe 观测,不发布或恢复 Agent,也不提交 cold repair。对于没有 live owner 的 Session,系统先用 `listSnapshots()` 判定明确不存在;已进入目录的 Session 若 `inspect()` 失败,仍属于基础设施故障,不会被猜测成 `session-not-found`。`put` 只接受具有指定 `MessageId` 的非空、append-origin `assistant/message`;replacement-origin 消息、仅承载 usage 的空 assistant 记录与非 assistant 记录都返回 `target-not-found`。
33
+
34
+ 初步校验后,`put` 在写入伴随记录前建立 durability barrier。身份匹配的 live Session 先通过权威 `ctx.sessions.flush` checkpoint 提交,随后 live 与 cold 路径都会通过 `SessionPersistence.readFrom` 从序列零做物理复读。之后再次校验所得观测的 header 身份与目标。缺少 flush 参与方、身份变化、目标消失或物理读取失败都会阻止伴随记录提交,因此持久反馈绝不会先于其持久目标消息。
35
+
36
+ message feedback 不是 Session 日志内容或 Session 投影。它不发出 `feedback/record` 事件,不进入模型历史,也不触发 `FEEDBACK_ONLY` 遥测释放。
37
+
38
+ ## 服务与 Host Remote 契约
39
+
40
+ `GatewayService` 与 `@Remote` 将 `MessageFeedbackService` 的同三个方法发布出去;Host endpoint 名称为 `messageFeedback.list`、`messageFeedback.put` 与 `messageFeedback.delete`。每个方法都返回判别式业务 union:`{ ok: true, value }` 或 `{ ok: false, error }`。存储、损坏或缺少 durability listener 等操作故障会产生 reject,不会被误标为业务错误。
41
+
42
+ | 方法 | 请求 | 成功 `value` | 拒绝的 `error.code` |
43
+ |---|---|---|---|
44
+ | `list` | `MessageFeedbackListRequest { sessionId }` | `MessageFeedbackListValue { items }` | `session-not-found` |
45
+ | `put` | `MessageFeedbackPutRequest { sessionId, messageId, rating, note?, ifVersion }` | 已提交的 `MessageFeedbackItem` | `session-not-found`、`target-not-found`、`version-conflict`、`note-blank`、`note-too-large` |
46
+ | `delete` | `MessageFeedbackDeleteRequest { sessionId, messageId, ifVersion }` | `MessageFeedbackDeleteValue { absent: true }` | `session-not-found`、`version-conflict` |
47
+
48
+ `MessageFeedbackVersionConflict` 返回权威 `current` 条目;条目不存在时为 `null`。调用方无需额外执行 `list`,即可协调当前 rating、note 与 version。`MessageFeedbackNoteTooLarge` 同时返回 `maxBytes` 与 `actualBytes`。客户端 Remote 聚合尚未挂载生成的客户端 contribution;Host 调用方无需该客户端组装即可使用 service/Remote 契约。
49
+
50
+ ## Compare-and-set 与幂等性
51
+
52
+ `ifVersion: null` 表示仅当条目不存在时才创建;已有条目的每次请求都必须与其当前 version 完全一致,即使目标值已经相同、不会产生实质更新。检查按消息而非按 Session 进行,因此修改一个条目不会与另一个条目冲突。每次实质创建或更新都会分配新的 opaque UUID token,防止陈旧写入穿过 ABA 值循环。
53
+
54
+ 携带匹配 version 的无变化请求会返回已存条目,version 与时间戳均不变。成功响应丢失后,使用旧 token 重试会得到 `version-conflict.current`;调用方无需额外读取,即可把权威当前值与目标值比较。条目已不存在时,`delete` 忽略 `ifVersion`;成功后始终返回稳定的 `{ absent: true }` 后置条件。
55
+
56
+ 按 Session 划分的 promise 队列覆盖检查、持久性校验、伴随记录读取、比较与整行写入。这些语义会串行化经由同一服务实例的并发变更;storage-domain 自身没有跨进程条件写。
57
+
58
+ Plugin disposal 会先关闭变更接纳,排空已进入各个 Session 队列的所有操作,然后才关闭 storage domain。disposal 开始后提交的变更会以生命周期故障拒绝,不会进入正在关闭的 domain。
59
+
60
+ ## 模型体验
61
+
62
+ ### 本地消息反馈状态
63
+
64
+ #### 模型看到的内容
65
+
66
+ 无。`ctx.messageFeedback` 不注册工具、提示词段落、模型可见上下文或 Session 事件;除非另一个具有独立文档的 Consumer 显式公开反馈,否则它只留在 Host 拥有的伴随记录中。
67
+
68
+ #### Token 影响
69
+
70
+ 为零。本包的请求、结果、评分、备注、时间戳或失败都不会进入模型请求。
71
+
72
+ #### KV Cache 影响
73
+
74
+ 相互独立。读取或变更消息反馈不会触碰模型请求前缀,也不会使本可复用的提供方缓存条目失效。
75
+
76
+ ## 已知局限与延后工作
77
+
78
+ - **缺少客户端聚合与 UI**——Host Remote 契约已经发布,但客户端 Remote 聚合 contribution 与任何 UI 消费方由各自边界负责并保持延后。
79
+ - **Compare-and-set 仅限单进程**——按 Session 划分的队列只串行化一个服务实例;storage-domain 不提供跨进程条件写,因此多个 Host 进程写入同一存储根目录时仍可能丢失更新。
80
+ - **没有持久 Session 删除级联**——Session persistence 没有删除接口,且 `session/disposed`/`host/session-removed` 表示 detach 而非持久删除。因此服务会保留空行,并可能在带外移除日志后留下孤儿行,而不会在 detach 时删除仍有效的反馈。
81
+ - **Detach/catalog retirement 窗口**——请求若恰好落在 live detach 之后、persistence catalog 物化 header 之前的极短窗口,可能收到 `session-not-found`;调用方应在 retirement materialization 后重试。
82
+ - **Header 身份不是内容指纹**——只有 `{createdAt, cwd}` 不同时才能识别复用;本契约无法区分保留相同 header 身份的克隆日志。
83
+ - **调用方边界受信任**——`list`/`put`/`delete` 不携带已认证的 actor 或审计身份。在加入授权与归属信息前,部署方必须只通过受信任或另行认证的边界暴露 Host gateway。
84
+ - **目录与行边界**——由于 persistence 没有按 id 读取元数据的操作,cold 请求会扫描完整的 Session snapshot 目录。`maxNoteBytes` 只限制单条备注,单个 Session 行的条目数和聚合保留字节尚无上限;按索引读取元数据和由部署决定的行边界,延后到具体消费方明确策略时处理。
package/lib/index.js ADDED
@@ -0,0 +1,420 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { randomUUID } from "node:crypto";
3
+ import { Service } from "@deepseek-ai/cordis";
4
+ import s from "@deepseek-ai/schemastery";
5
+ import { deriveEventMessage, isAppendSurfaceEvent } from "@deepseek-ai/dsh-session/surface";
6
+ import { GatewayService, Remote } from "@deepseek-ai/dsh-type-meta";
7
+ import { z } from "zod";
8
+ import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
9
+ //#region lib/types/spec.js
10
+ /**
11
+ * Durable storage-domain declaration for lifecycle-bound message feedback.
12
+ * @module @deepseek-ai/dsh-message-feedback/src/spec
13
+ */
14
+ const nonNegativeSafeInteger = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
15
+ /** Runtime schema for the closed rating vocabulary. */
16
+ const messageFeedbackRatingSchema = z.union([z.literal("positive"), z.literal("negative")]);
17
+ /** Runtime schema for one opaque item version stored on disk. */
18
+ const messageFeedbackVersionSchema = z.uuid().transform((value) => value);
19
+ /** Runtime schema for one current feedback item. */
20
+ const messageFeedbackItemSchema = z.object({
21
+ messageId: z.string().min(1).transform((value) => value),
22
+ rating: messageFeedbackRatingSchema,
23
+ note: z.string().refine((note) => note.trim().length > 0, { message: "message feedback note must contain a non-whitespace character" }).optional(),
24
+ version: messageFeedbackVersionSchema,
25
+ createdAt: nonNegativeSafeInteger,
26
+ updatedAt: nonNegativeSafeInteger
27
+ }).refine((item) => item.updatedAt >= item.createdAt, {
28
+ path: ["updatedAt"],
29
+ message: "message feedback updatedAt must not precede createdAt"
30
+ });
31
+ /** Persisted Session fields that fence a sidecar row to one log lifecycle. */
32
+ const messageFeedbackSessionIdentitySchema = z.object({
33
+ createdAt: nonNegativeSafeInteger,
34
+ cwd: z.string().optional()
35
+ });
36
+ /**
37
+ * One whole-Session sidecar. Duplicate message ids would make item lookup
38
+ * ambiguous; duplicate versions would break their independent identity.
39
+ */
40
+ const messageFeedbackRowSchema = z.object({
41
+ session: messageFeedbackSessionIdentitySchema,
42
+ items: z.array(messageFeedbackItemSchema)
43
+ }).superRefine((row, ctx) => {
44
+ const messageIds = /* @__PURE__ */ new Set();
45
+ const versions = /* @__PURE__ */ new Set();
46
+ row.items.forEach((item, index) => {
47
+ if (messageIds.has(item.messageId)) ctx.addIssue({
48
+ code: "custom",
49
+ path: [
50
+ "items",
51
+ index,
52
+ "messageId"
53
+ ],
54
+ message: `duplicate message feedback id '${item.messageId}'`
55
+ });
56
+ messageIds.add(item.messageId);
57
+ if (versions.has(item.version)) ctx.addIssue({
58
+ code: "custom",
59
+ path: [
60
+ "items",
61
+ index,
62
+ "version"
63
+ ],
64
+ message: `duplicate message feedback version '${item.version}'`
65
+ });
66
+ versions.add(item.version);
67
+ });
68
+ });
69
+ /** One lifecycle-bound sidecar record per Session id. */
70
+ const messageFeedbackDomainSpec = defineDomain({
71
+ name: "message_feedback",
72
+ version: 0,
73
+ tables: { sessions: domainTable(messageFeedbackRowSchema) }
74
+ });
75
+ //#endregion
76
+ //#region lib/types/index.js
77
+ /**
78
+ * Durable, lifecycle-bound feedback for finalized assistant messages.
79
+ * @module @deepseek-ai/dsh-message-feedback
80
+ */
81
+ var __runInitializers = function(thisArg, initializers, value) {
82
+ var useValue = arguments.length > 2;
83
+ for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
84
+ return useValue ? value : void 0;
85
+ };
86
+ var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
87
+ function accept(f) {
88
+ if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected");
89
+ return f;
90
+ }
91
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
92
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
93
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
94
+ var _, done = false;
95
+ for (var i = decorators.length - 1; i >= 0; i--) {
96
+ var context = {};
97
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
98
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
99
+ context.addInitializer = function(f) {
100
+ if (done) throw new TypeError("Cannot add initializers after decoration has completed");
101
+ extraInitializers.push(accept(f || null));
102
+ };
103
+ var result = (0, decorators[i])(kind === "accessor" ? {
104
+ get: descriptor.get,
105
+ set: descriptor.set
106
+ } : descriptor[key], context);
107
+ if (kind === "accessor") {
108
+ if (result === void 0) continue;
109
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
110
+ if (_ = accept(result.get)) descriptor.get = _;
111
+ if (_ = accept(result.set)) descriptor.set = _;
112
+ if (_ = accept(result.init)) initializers.unshift(_);
113
+ } else if (_ = accept(result)) if (kind === "field") initializers.unshift(_);
114
+ else descriptor[key] = _;
115
+ }
116
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
117
+ done = true;
118
+ };
119
+ /** Immutable empty list reused only as an input to caller-owned copying. */
120
+ const EMPTY_ITEMS = Object.freeze([]);
121
+ /** Validate the one deployment-varying limit at the configuration boundary. */
122
+ function resolveMaxNoteBytes(value) {
123
+ if (!Number.isSafeInteger(value) || value < 1) throw new TypeError(`message-feedback: maxNoteBytes must be a positive safe integer, got ${String(value)}`);
124
+ return value;
125
+ }
126
+ /** Copy and freeze one item before it crosses the service boundary. */
127
+ function snapshotItem(item) {
128
+ return Object.freeze({
129
+ messageId: item.messageId,
130
+ rating: item.rating,
131
+ ...item.note === void 0 ? {} : { note: item.note },
132
+ version: item.version,
133
+ createdAt: item.createdAt,
134
+ updatedAt: item.updatedAt
135
+ });
136
+ }
137
+ /** Copy and freeze a list response. */
138
+ function snapshotList(items) {
139
+ return Object.freeze({ items: Object.freeze(items.map(snapshotItem)) });
140
+ }
141
+ /** Build a frozen success branch. */
142
+ function success(value) {
143
+ return Object.freeze({
144
+ ok: true,
145
+ value
146
+ });
147
+ }
148
+ /** Build a frozen business-failure branch. */
149
+ function rejected(error) {
150
+ return Object.freeze({
151
+ ok: false,
152
+ error: Object.freeze(error)
153
+ });
154
+ }
155
+ /** Project the Session fields that distinguish one persisted log lifecycle. */
156
+ function identityOf(header) {
157
+ return Object.freeze({
158
+ createdAt: header.createdAt,
159
+ ...header.cwd === void 0 ? {} : { cwd: header.cwd }
160
+ });
161
+ }
162
+ /** Whether a stored row belongs to the inspected Session lifecycle. */
163
+ function sameIdentity(row, header) {
164
+ return row.session.createdAt === header.createdAt && row.session.cwd === header.cwd;
165
+ }
166
+ /** Whether two observations name the same persisted Session lifecycle. */
167
+ function sameHeaderIdentity(left, right) {
168
+ return left.id === right.id && left.createdAt === right.createdAt && left.cwd === right.cwd;
169
+ }
170
+ /** Freeze the replacement row so storage-domain never exposes mutable aliases. */
171
+ function rowSnapshot(session, items) {
172
+ const copiedItems = items.map(snapshotItem);
173
+ Object.freeze(copiedItems);
174
+ return Object.freeze({
175
+ session,
176
+ items: copiedItems
177
+ });
178
+ }
179
+ /** Generate an opaque equality token for one material mutation. */
180
+ function nextVersion() {
181
+ return randomUUID();
182
+ }
183
+ /**
184
+ * Storage-domain sidecar service. It inspects persisted Session history and
185
+ * never creates or resumes an Agent or Session.
186
+ */
187
+ let MessageFeedbackService = (() => {
188
+ let _classSuper = GatewayService;
189
+ let _instanceExtraInitializers = [];
190
+ let _list_decorators;
191
+ let _put_decorators;
192
+ let _delete_decorators;
193
+ return class MessageFeedbackService extends _classSuper {
194
+ static {
195
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
196
+ _list_decorators = [Remote("list")];
197
+ _put_decorators = [Remote("put")];
198
+ _delete_decorators = [Remote("delete")];
199
+ __esDecorate(this, null, _list_decorators, {
200
+ kind: "method",
201
+ name: "list",
202
+ static: false,
203
+ private: false,
204
+ access: {
205
+ has: (obj) => "list" in obj,
206
+ get: (obj) => obj.list
207
+ },
208
+ metadata: _metadata
209
+ }, null, _instanceExtraInitializers);
210
+ __esDecorate(this, null, _put_decorators, {
211
+ kind: "method",
212
+ name: "put",
213
+ static: false,
214
+ private: false,
215
+ access: {
216
+ has: (obj) => "put" in obj,
217
+ get: (obj) => obj.put
218
+ },
219
+ metadata: _metadata
220
+ }, null, _instanceExtraInitializers);
221
+ __esDecorate(this, null, _delete_decorators, {
222
+ kind: "method",
223
+ name: "delete",
224
+ static: false,
225
+ private: false,
226
+ access: {
227
+ has: (obj) => "delete" in obj,
228
+ get: (obj) => obj.delete
229
+ },
230
+ metadata: _metadata
231
+ }, null, _instanceExtraInitializers);
232
+ if (_metadata) Object.defineProperty(this, Symbol.metadata, {
233
+ enumerable: true,
234
+ configurable: true,
235
+ writable: true,
236
+ value: _metadata
237
+ });
238
+ }
239
+ static inject = [
240
+ "storageDomain",
241
+ "sessionPersistence",
242
+ "sessions"
243
+ ];
244
+ /** Loader validation for the required note-size policy. */
245
+ static Config = s.object({ maxNoteBytes: s.number().step(1).min(1).required() });
246
+ maxNoteBytes = __runInitializers(this, _instanceExtraInitializers);
247
+ table;
248
+ operationTails = /* @__PURE__ */ new Map();
249
+ mutationAdmissionOpen = true;
250
+ /**
251
+ * @param ctx - Host context carrying persistence and the storage-domain form.
252
+ * @param config - Required note-size policy.
253
+ */
254
+ constructor(ctx, config) {
255
+ super(ctx, "messageFeedback");
256
+ this.maxNoteBytes = resolveMaxNoteBytes(config.maxNoteBytes);
257
+ }
258
+ /** Open and own the one message-feedback sidecar domain. */
259
+ async [Service.init]() {
260
+ const domain = await this.ctx.storageDomain.open(messageFeedbackDomainSpec);
261
+ this.ctx.effect(() => async () => {
262
+ this.mutationAdmissionOpen = false;
263
+ await Promise.all(this.operationTails.values());
264
+ await domain.close();
265
+ }, "message-feedback.domainClose");
266
+ this.table = domain.table("sessions");
267
+ }
268
+ /**
269
+ * Read feedback belonging to the current persisted Session lifecycle.
270
+ * A stale row from a reused Session id is invisible.
271
+ * @param request - Session identity to inspect and list.
272
+ * @returns current immutable items or `session-not-found`.
273
+ */
274
+ async list(request) {
275
+ const known = await this.inspectSession(request.sessionId);
276
+ if (!known.ok) return known;
277
+ const row = this.requireTable().get(request.sessionId);
278
+ return success(snapshotList(row !== void 0 && sameIdentity(row, known.value.meta) ? row.items : EMPTY_ITEMS));
279
+ }
280
+ /**
281
+ * Create or replace feedback for one derived append-origin assistant
282
+ * message. Every request must match the addressed item's current version;
283
+ * a matching no-op returns the stored item without changing its revision.
284
+ * @param request - target, desired value, and observed item version.
285
+ * @returns the committed item or an explicit business failure.
286
+ */
287
+ put(request) {
288
+ const note = this.resolveNote(request.note);
289
+ if (!note.ok) return Promise.resolve(note);
290
+ return this.enqueue(request.sessionId, async () => {
291
+ const known = await this.inspectSession(request.sessionId);
292
+ if (!known.ok) return known;
293
+ if (!this.hasFeedbackTarget(known.value, request.messageId)) return rejected({
294
+ code: "target-not-found",
295
+ sessionId: request.sessionId,
296
+ messageId: request.messageId
297
+ });
298
+ const durable = await this.ensureTargetDurable(known.value);
299
+ if (!sameHeaderIdentity(durable.meta, known.value.meta) || !this.hasFeedbackTarget(durable, request.messageId)) return rejected({
300
+ code: "target-not-found",
301
+ sessionId: request.sessionId,
302
+ messageId: request.messageId
303
+ });
304
+ const table = this.requireTable();
305
+ const stored = table.get(request.sessionId);
306
+ const items = (stored !== void 0 && sameIdentity(stored, durable.meta) ? stored : void 0)?.items ?? EMPTY_ITEMS;
307
+ const index = items.findIndex((item) => item.messageId === request.messageId);
308
+ const existing = items[index];
309
+ if (request.ifVersion !== (existing?.version ?? null)) return rejected(this.versionConflict(existing ?? null));
310
+ if (existing !== void 0 && existing.rating === request.rating && existing.note === note.value) return success(snapshotItem(existing));
311
+ const now = Date.now();
312
+ const item = snapshotItem({
313
+ messageId: request.messageId,
314
+ rating: request.rating,
315
+ ...note.value === void 0 ? {} : { note: note.value },
316
+ version: nextVersion(),
317
+ createdAt: existing?.createdAt ?? now,
318
+ updatedAt: existing === void 0 ? now : Math.max(now, existing.updatedAt)
319
+ });
320
+ const nextItems = [...items];
321
+ if (index === -1) nextItems.push(item);
322
+ else nextItems[index] = item;
323
+ await table.put(request.sessionId, rowSnapshot(identityOf(durable.meta), nextItems));
324
+ return success(snapshotItem(item));
325
+ });
326
+ }
327
+ /**
328
+ * Delete one feedback item. Absence is successful regardless of the
329
+ * supplied version; an existing item requires an exact version match.
330
+ * @param request - Session, message, and observed item version.
331
+ * @returns the stable absent postcondition, or an explicit failure.
332
+ */
333
+ delete(request) {
334
+ return this.enqueue(request.sessionId, async () => {
335
+ const known = await this.inspectSession(request.sessionId);
336
+ if (!known.ok) return known;
337
+ const table = this.requireTable();
338
+ const stored = table.get(request.sessionId);
339
+ const items = (stored !== void 0 && sameIdentity(stored, known.value.meta) ? stored : void 0)?.items ?? EMPTY_ITEMS;
340
+ const existing = items.find((item) => item.messageId === request.messageId);
341
+ if (existing === void 0) return success(Object.freeze({ absent: true }));
342
+ if (request.ifVersion !== existing.version) return rejected(this.versionConflict(existing));
343
+ await table.put(request.sessionId, rowSnapshot(identityOf(known.value.meta), items.filter((item) => item !== existing)));
344
+ return success(Object.freeze({ absent: true }));
345
+ });
346
+ }
347
+ /**
348
+ * Resolve a live owner directly; otherwise use the storage catalog as the
349
+ * existence authority before inspecting the log. Inspection failures for a
350
+ * catalogued Session remain infrastructure failures rather than being
351
+ * guessed into the business `session-not-found` branch.
352
+ */
353
+ async inspectSession(sessionId) {
354
+ if (this.ctx.sessions.get(sessionId) === void 0) {
355
+ if (!(await this.ctx.sessionPersistence.listSnapshots()).some((snapshot) => snapshot.header.id === sessionId) && this.ctx.sessions.get(sessionId) === void 0) return rejected({
356
+ code: "session-not-found",
357
+ sessionId
358
+ });
359
+ }
360
+ return success(await this.ctx.sessionPersistence.inspect(sessionId));
361
+ }
362
+ /** Require the exact finalized append-origin assistant message projection. */
363
+ hasFeedbackTarget(inspection, messageId) {
364
+ return inspection.events.some((event) => {
365
+ if (event.type !== "assistant/message" || !isAppendSurfaceEvent(event)) return false;
366
+ const message = deriveEventMessage(event);
367
+ return message?.role === "assistant" && message.id === messageId;
368
+ });
369
+ }
370
+ /**
371
+ * Put the target log prefix behind a durability barrier before its sidecar.
372
+ * A live owner flushes through the SessionStore's canonical checkpoint; a
373
+ * cold owner is re-read from the physical durable prefix.
374
+ */
375
+ async ensureTargetDurable(inspection) {
376
+ const live = this.ctx.sessions.get(inspection.meta.id);
377
+ if (live !== void 0 && sameHeaderIdentity(live.header, inspection.meta)) {
378
+ if (!await this.ctx.sessions.flush(live)) throw new Error(`message-feedback: no durability listener participated for live session '${inspection.meta.id}'`);
379
+ return await this.ctx.sessionPersistence.readFrom(inspection.meta.id, 0);
380
+ }
381
+ return await this.ctx.sessionPersistence.readFrom(inspection.meta.id, 0);
382
+ }
383
+ /** Validate optional-note semantics and the configured complete UTF-8 byte bound. */
384
+ resolveNote(note) {
385
+ if (note === void 0) return success(void 0);
386
+ if (note.trim().length === 0) return rejected({ code: "note-blank" });
387
+ const actualBytes = Buffer.byteLength(note, "utf8");
388
+ if (actualBytes > this.maxNoteBytes) return rejected({
389
+ code: "note-too-large",
390
+ maxBytes: this.maxNoteBytes,
391
+ actualBytes
392
+ });
393
+ return success(note);
394
+ }
395
+ /** Return the authoritative item needed to reconcile one failed comparison. */
396
+ versionConflict(current) {
397
+ return {
398
+ code: "version-conflict",
399
+ current: current === null ? null : snapshotItem(current)
400
+ };
401
+ }
402
+ /** Queue a complete read/compare/write mutation behind this Session's prior mutation. */
403
+ enqueue(sessionId, operation) {
404
+ if (!this.mutationAdmissionOpen) return Promise.reject(/* @__PURE__ */ new Error("message-feedback: service is disposing"));
405
+ const result = (this.operationTails.get(sessionId) ?? Promise.resolve()).then(operation);
406
+ const tail = result.then(() => void 0, () => void 0);
407
+ this.operationTails.set(sessionId, tail);
408
+ return result.finally(() => {
409
+ if (this.operationTails.get(sessionId) === tail) this.operationTails.delete(sessionId);
410
+ });
411
+ }
412
+ /** Resolve the initialized durable table or fail a broken service lifecycle. */
413
+ requireTable() {
414
+ if (this.table === void 0) throw new Error("message-feedback: durable domain is not initialized");
415
+ return this.table;
416
+ }
417
+ };
418
+ })();
419
+ //#endregion
420
+ export { MessageFeedbackService, MessageFeedbackService as default, messageFeedbackDomainSpec, messageFeedbackItemSchema, messageFeedbackRatingSchema, messageFeedbackRowSchema, messageFeedbackSessionIdentitySchema, messageFeedbackVersionSchema };
@@ -0,0 +1,20 @@
1
+ //#region lib/types/invariant.js
2
+ /** Package-owned invariant companion. @module @deepseek-ai/dsh-message-feedback/invariant */
3
+ const PACKAGE_NAME = "@deepseek-ai/dsh-message-feedback";
4
+ /** Cordis companion plugin name. */
5
+ const name = "message-feedback-invariant";
6
+ /** Services required before the companion can reserve and check package ownership. */
7
+ const inject = ["invariants"];
8
+ /**
9
+ * No runtime invariant: the private typed writer owns current row mutations,
10
+ * the domain schema validates rows on reopen, and no second authority exists.
11
+ */
12
+ const install = Object.assign(() => {}, { inject: ["messageFeedback"] });
13
+ /**
14
+ * Register this package's invariant companion.
15
+ * @param ctx - Cordis context carrying the invariant service.
16
+ * @returns the installed registration's disposer after setup succeeds.
17
+ */
18
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
19
+ //#endregion
20
+ export { apply, inject, name };
@@ -0,0 +1,3 @@
1
+ /* Generated by @deepseek-ai/dsh-typert-generator from FaceModel — do not edit. */
2
+
3
+ export declare const TYPERT: unknown