@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.
@@ -0,0 +1,342 @@
1
+ /**
2
+ * Durable, lifecycle-bound feedback for finalized assistant messages.
3
+ * @module @deepseek-ai/dsh-message-feedback
4
+ */
5
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
6
+ var useValue = arguments.length > 2;
7
+ for (var i = 0; i < initializers.length; i++) {
8
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
9
+ }
10
+ return useValue ? value : void 0;
11
+ };
12
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
13
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
14
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
15
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
16
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
17
+ var _, done = false;
18
+ for (var i = decorators.length - 1; i >= 0; i--) {
19
+ var context = {};
20
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
21
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
22
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
23
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
24
+ if (kind === "accessor") {
25
+ if (result === void 0) continue;
26
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
27
+ if (_ = accept(result.get)) descriptor.get = _;
28
+ if (_ = accept(result.set)) descriptor.set = _;
29
+ if (_ = accept(result.init)) initializers.unshift(_);
30
+ }
31
+ else if (_ = accept(result)) {
32
+ if (kind === "field") initializers.unshift(_);
33
+ else descriptor[key] = _;
34
+ }
35
+ }
36
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
37
+ done = true;
38
+ };
39
+ import { Buffer } from 'node:buffer';
40
+ import { randomUUID } from 'node:crypto';
41
+ import { Service } from '@deepseek-ai/cordis';
42
+ import s from '@deepseek-ai/schemastery';
43
+ import { deriveEventMessage, isAppendSurfaceEvent } from '@deepseek-ai/dsh-session/surface';
44
+ import { GatewayService, Remote } from '@deepseek-ai/dsh-type-meta';
45
+ import { messageFeedbackDomainSpec } from "./spec.js";
46
+ export { messageFeedbackDomainSpec, messageFeedbackItemSchema, messageFeedbackRatingSchema, messageFeedbackRowSchema, messageFeedbackSessionIdentitySchema, messageFeedbackVersionSchema, } from "./spec.js";
47
+ /** Immutable empty list reused only as an input to caller-owned copying. */
48
+ const EMPTY_ITEMS = Object.freeze([]);
49
+ /** Validate the one deployment-varying limit at the configuration boundary. */
50
+ function resolveMaxNoteBytes(value) {
51
+ if (!Number.isSafeInteger(value) || value < 1) {
52
+ throw new TypeError(`message-feedback: maxNoteBytes must be a positive safe integer, got ${String(value)}`);
53
+ }
54
+ return value;
55
+ }
56
+ /** Copy and freeze one item before it crosses the service boundary. */
57
+ function snapshotItem(item) {
58
+ return Object.freeze({
59
+ messageId: item.messageId,
60
+ rating: item.rating,
61
+ ...(item.note === undefined ? {} : { note: item.note }),
62
+ version: item.version,
63
+ createdAt: item.createdAt,
64
+ updatedAt: item.updatedAt,
65
+ });
66
+ }
67
+ /** Copy and freeze a list response. */
68
+ function snapshotList(items) {
69
+ return Object.freeze({ items: Object.freeze(items.map(snapshotItem)) });
70
+ }
71
+ /** Build a frozen success branch. */
72
+ function success(value) {
73
+ return Object.freeze({ ok: true, value });
74
+ }
75
+ /** Build a frozen business-failure branch. */
76
+ function rejected(error) {
77
+ return Object.freeze({ ok: false, error: Object.freeze(error) });
78
+ }
79
+ /** Project the Session fields that distinguish one persisted log lifecycle. */
80
+ function identityOf(header) {
81
+ return Object.freeze({
82
+ createdAt: header.createdAt,
83
+ ...(header.cwd === undefined ? {} : { cwd: header.cwd }),
84
+ });
85
+ }
86
+ /** Whether a stored row belongs to the inspected Session lifecycle. */
87
+ function sameIdentity(row, header) {
88
+ return row.session.createdAt === header.createdAt && row.session.cwd === header.cwd;
89
+ }
90
+ /** Whether two observations name the same persisted Session lifecycle. */
91
+ function sameHeaderIdentity(left, right) {
92
+ return left.id === right.id && left.createdAt === right.createdAt && left.cwd === right.cwd;
93
+ }
94
+ /** Freeze the replacement row so storage-domain never exposes mutable aliases. */
95
+ function rowSnapshot(session, items) {
96
+ const copiedItems = items.map(snapshotItem);
97
+ Object.freeze(copiedItems);
98
+ return Object.freeze({
99
+ session,
100
+ items: copiedItems,
101
+ });
102
+ }
103
+ /** Generate an opaque equality token for one material mutation. */
104
+ function nextVersion() {
105
+ return randomUUID();
106
+ }
107
+ /**
108
+ * Storage-domain sidecar service. It inspects persisted Session history and
109
+ * never creates or resumes an Agent or Session.
110
+ */
111
+ let MessageFeedbackService = (() => {
112
+ let _classSuper = GatewayService;
113
+ let _instanceExtraInitializers = [];
114
+ let _list_decorators;
115
+ let _put_decorators;
116
+ let _delete_decorators;
117
+ return class MessageFeedbackService extends _classSuper {
118
+ static {
119
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
120
+ _list_decorators = [Remote('list')];
121
+ _put_decorators = [Remote('put')];
122
+ _delete_decorators = [Remote('delete')];
123
+ __esDecorate(this, null, _list_decorators, { kind: "method", name: "list", static: false, private: false, access: { has: obj => "list" in obj, get: obj => obj.list }, metadata: _metadata }, null, _instanceExtraInitializers);
124
+ __esDecorate(this, null, _put_decorators, { kind: "method", name: "put", static: false, private: false, access: { has: obj => "put" in obj, get: obj => obj.put }, metadata: _metadata }, null, _instanceExtraInitializers);
125
+ __esDecorate(this, null, _delete_decorators, { kind: "method", name: "delete", static: false, private: false, access: { has: obj => "delete" in obj, get: obj => obj.delete }, metadata: _metadata }, null, _instanceExtraInitializers);
126
+ if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
127
+ }
128
+ static inject = ['storageDomain', 'sessionPersistence', 'sessions'];
129
+ /** Loader validation for the required note-size policy. */
130
+ static Config = s.object({
131
+ maxNoteBytes: s.number().step(1).min(1).required(),
132
+ });
133
+ maxNoteBytes = __runInitializers(this, _instanceExtraInitializers);
134
+ table;
135
+ operationTails = new Map();
136
+ mutationAdmissionOpen = true;
137
+ /**
138
+ * @param ctx - Host context carrying persistence and the storage-domain form.
139
+ * @param config - Required note-size policy.
140
+ */
141
+ constructor(ctx, config) {
142
+ super(ctx, 'messageFeedback');
143
+ this.maxNoteBytes = resolveMaxNoteBytes(config.maxNoteBytes);
144
+ }
145
+ /** Open and own the one message-feedback sidecar domain. */
146
+ async [Service.init]() {
147
+ const domain = await this.ctx.storageDomain.open(messageFeedbackDomainSpec);
148
+ this.ctx.effect(() => async () => {
149
+ this.mutationAdmissionOpen = false;
150
+ await Promise.all(this.operationTails.values());
151
+ await domain.close();
152
+ }, 'message-feedback.domainClose');
153
+ this.table = domain.table('sessions');
154
+ }
155
+ /**
156
+ * Read feedback belonging to the current persisted Session lifecycle.
157
+ * A stale row from a reused Session id is invisible.
158
+ * @param request - Session identity to inspect and list.
159
+ * @returns current immutable items or `session-not-found`.
160
+ */
161
+ async list(request) {
162
+ const known = await this.inspectSession(request.sessionId);
163
+ if (!known.ok)
164
+ return known;
165
+ const row = this.requireTable().get(request.sessionId);
166
+ const items = row !== undefined && sameIdentity(row, known.value.meta) ? row.items : EMPTY_ITEMS;
167
+ return success(snapshotList(items));
168
+ }
169
+ /**
170
+ * Create or replace feedback for one derived append-origin assistant
171
+ * message. Every request must match the addressed item's current version;
172
+ * a matching no-op returns the stored item without changing its revision.
173
+ * @param request - target, desired value, and observed item version.
174
+ * @returns the committed item or an explicit business failure.
175
+ */
176
+ put(request) {
177
+ const note = this.resolveNote(request.note);
178
+ if (!note.ok)
179
+ return Promise.resolve(note);
180
+ return this.enqueue(request.sessionId, async () => {
181
+ const known = await this.inspectSession(request.sessionId);
182
+ if (!known.ok)
183
+ return known;
184
+ if (!this.hasFeedbackTarget(known.value, request.messageId)) {
185
+ return rejected({
186
+ code: 'target-not-found',
187
+ sessionId: request.sessionId,
188
+ messageId: request.messageId,
189
+ });
190
+ }
191
+ const durable = await this.ensureTargetDurable(known.value);
192
+ if (!sameHeaderIdentity(durable.meta, known.value.meta)
193
+ || !this.hasFeedbackTarget(durable, request.messageId)) {
194
+ return rejected({
195
+ code: 'target-not-found',
196
+ sessionId: request.sessionId,
197
+ messageId: request.messageId,
198
+ });
199
+ }
200
+ const table = this.requireTable();
201
+ const stored = table.get(request.sessionId);
202
+ const current = stored !== undefined && sameIdentity(stored, durable.meta) ? stored : undefined;
203
+ const items = current?.items ?? EMPTY_ITEMS;
204
+ const index = items.findIndex(item => item.messageId === request.messageId);
205
+ const existing = items[index];
206
+ if (request.ifVersion !== (existing?.version ?? null)) {
207
+ return rejected(this.versionConflict(existing ?? null));
208
+ }
209
+ if (existing !== undefined
210
+ && existing.rating === request.rating
211
+ && existing.note === note.value) {
212
+ return success(snapshotItem(existing));
213
+ }
214
+ const now = Date.now();
215
+ const item = snapshotItem({
216
+ messageId: request.messageId,
217
+ rating: request.rating,
218
+ ...(note.value === undefined ? {} : { note: note.value }),
219
+ version: nextVersion(),
220
+ createdAt: existing?.createdAt ?? now,
221
+ updatedAt: existing === undefined ? now : Math.max(now, existing.updatedAt),
222
+ });
223
+ const nextItems = [...items];
224
+ if (index === -1)
225
+ nextItems.push(item);
226
+ else
227
+ nextItems[index] = item;
228
+ await table.put(request.sessionId, rowSnapshot(identityOf(durable.meta), nextItems));
229
+ return success(snapshotItem(item));
230
+ });
231
+ }
232
+ /**
233
+ * Delete one feedback item. Absence is successful regardless of the
234
+ * supplied version; an existing item requires an exact version match.
235
+ * @param request - Session, message, and observed item version.
236
+ * @returns the stable absent postcondition, or an explicit failure.
237
+ */
238
+ delete(request) {
239
+ return this.enqueue(request.sessionId, async () => {
240
+ const known = await this.inspectSession(request.sessionId);
241
+ if (!known.ok)
242
+ return known;
243
+ const table = this.requireTable();
244
+ const stored = table.get(request.sessionId);
245
+ const current = stored !== undefined && sameIdentity(stored, known.value.meta) ? stored : undefined;
246
+ const items = current?.items ?? EMPTY_ITEMS;
247
+ const existing = items.find(item => item.messageId === request.messageId);
248
+ if (existing === undefined) {
249
+ return success(Object.freeze({ absent: true }));
250
+ }
251
+ if (request.ifVersion !== existing.version) {
252
+ return rejected(this.versionConflict(existing));
253
+ }
254
+ await table.put(request.sessionId, rowSnapshot(identityOf(known.value.meta), items.filter(item => item !== existing)));
255
+ return success(Object.freeze({ absent: true }));
256
+ });
257
+ }
258
+ /**
259
+ * Resolve a live owner directly; otherwise use the storage catalog as the
260
+ * existence authority before inspecting the log. Inspection failures for a
261
+ * catalogued Session remain infrastructure failures rather than being
262
+ * guessed into the business `session-not-found` branch.
263
+ */
264
+ async inspectSession(sessionId) {
265
+ if (this.ctx.sessions.get(sessionId) === undefined) {
266
+ const snapshots = await this.ctx.sessionPersistence.listSnapshots();
267
+ if (!snapshots.some(snapshot => snapshot.header.id === sessionId)
268
+ && this.ctx.sessions.get(sessionId) === undefined) {
269
+ return rejected({ code: 'session-not-found', sessionId });
270
+ }
271
+ }
272
+ return success(await this.ctx.sessionPersistence.inspect(sessionId));
273
+ }
274
+ /** Require the exact finalized append-origin assistant message projection. */
275
+ hasFeedbackTarget(inspection, messageId) {
276
+ return inspection.events.some((event) => {
277
+ if (event.type !== 'assistant/message' || !isAppendSurfaceEvent(event))
278
+ return false;
279
+ const message = deriveEventMessage(event);
280
+ return message?.role === 'assistant' && message.id === messageId;
281
+ });
282
+ }
283
+ /**
284
+ * Put the target log prefix behind a durability barrier before its sidecar.
285
+ * A live owner flushes through the SessionStore's canonical checkpoint; a
286
+ * cold owner is re-read from the physical durable prefix.
287
+ */
288
+ async ensureTargetDurable(inspection) {
289
+ const live = this.ctx.sessions.get(inspection.meta.id);
290
+ if (live !== undefined && sameHeaderIdentity(live.header, inspection.meta)) {
291
+ if (!(await this.ctx.sessions.flush(live))) {
292
+ throw new Error(`message-feedback: no durability listener participated for live session '${inspection.meta.id}'`);
293
+ }
294
+ return await this.ctx.sessionPersistence.readFrom(inspection.meta.id, 0);
295
+ }
296
+ return await this.ctx.sessionPersistence.readFrom(inspection.meta.id, 0);
297
+ }
298
+ /** Validate optional-note semantics and the configured complete UTF-8 byte bound. */
299
+ resolveNote(note) {
300
+ if (note === undefined)
301
+ return success(undefined);
302
+ if (note.trim().length === 0)
303
+ return rejected({ code: 'note-blank' });
304
+ const actualBytes = Buffer.byteLength(note, 'utf8');
305
+ if (actualBytes > this.maxNoteBytes) {
306
+ return rejected({ code: 'note-too-large', maxBytes: this.maxNoteBytes, actualBytes });
307
+ }
308
+ return success(note);
309
+ }
310
+ /** Return the authoritative item needed to reconcile one failed comparison. */
311
+ versionConflict(current) {
312
+ return {
313
+ code: 'version-conflict',
314
+ current: current === null ? null : snapshotItem(current),
315
+ };
316
+ }
317
+ /** Queue a complete read/compare/write mutation behind this Session's prior mutation. */
318
+ enqueue(sessionId, operation) {
319
+ if (!this.mutationAdmissionOpen) {
320
+ return Promise.reject(new Error('message-feedback: service is disposing'));
321
+ }
322
+ const previous = this.operationTails.get(sessionId) ?? Promise.resolve();
323
+ const result = previous.then(operation);
324
+ const tail = result.then(() => undefined, () => undefined);
325
+ this.operationTails.set(sessionId, tail);
326
+ return result.finally(() => {
327
+ if (this.operationTails.get(sessionId) === tail)
328
+ this.operationTails.delete(sessionId);
329
+ });
330
+ }
331
+ /** Resolve the initialized durable table or fail a broken service lifecycle. */
332
+ requireTable() {
333
+ if (this.table === undefined) {
334
+ throw new Error('message-feedback: durable domain is not initialized');
335
+ }
336
+ return this.table;
337
+ }
338
+ };
339
+ })();
340
+ export { MessageFeedbackService };
341
+ export default MessageFeedbackService;
342
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,13 @@
1
+ /** Package-owned invariant companion. @module @deepseek-ai/dsh-message-feedback/invariant */
2
+ import type { Context } from '@deepseek-ai/cordis';
3
+ /** Cordis companion plugin name. */
4
+ export declare const name = "message-feedback-invariant";
5
+ /** Services required before the companion can reserve and check package ownership. */
6
+ export declare const inject: string[];
7
+ /**
8
+ * Register this package's invariant companion.
9
+ * @param ctx - Cordis context carrying the invariant service.
10
+ * @returns the installed registration's disposer after setup succeeds.
11
+ */
12
+ export declare const apply: (ctx: Context) => Promise<() => void>;
13
+ //# sourceMappingURL=invariant.d.ts.map
@@ -0,0 +1,19 @@
1
+ /** Package-owned invariant companion. @module @deepseek-ai/dsh-message-feedback/invariant */
2
+ const PACKAGE_NAME = '@deepseek-ai/dsh-message-feedback';
3
+ /** Cordis companion plugin name. */
4
+ export const name = 'message-feedback-invariant';
5
+ /** Services required before the companion can reserve and check package ownership. */
6
+ export const inject = ['invariants'];
7
+ /**
8
+ * No runtime invariant: the private typed writer owns current row mutations,
9
+ * the domain schema validates rows on reopen, and no second authority exists.
10
+ */
11
+ const install = Object.assign(() => { }, { inject: ['messageFeedback'] });
12
+ /**
13
+ * Register this package's invariant companion.
14
+ * @param ctx - Cordis context carrying the invariant service.
15
+ * @returns the installed registration's disposer after setup succeeds.
16
+ */
17
+ export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
18
+ /* jscpd:ignore-end */
19
+ //# sourceMappingURL=invariant.js.map
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Durable storage-domain declaration for lifecycle-bound message feedback.
3
+ * @module @deepseek-ai/dsh-message-feedback/src/spec
4
+ */
5
+ import { z } from 'zod';
6
+ import type { SessionId } from '@deepseek-ai/dsh-session/types';
7
+ import type { MessageFeedbackItem, MessageFeedbackVersion } from './types.ts';
8
+ /** Runtime schema for the closed rating vocabulary. */
9
+ export declare const messageFeedbackRatingSchema: z.ZodUnion<readonly [z.ZodLiteral<"positive">, z.ZodLiteral<"negative">]>;
10
+ /** Runtime schema for one opaque item version stored on disk. */
11
+ export declare const messageFeedbackVersionSchema: z.ZodPipe<z.ZodUUID, z.ZodTransform<MessageFeedbackVersion, string>>;
12
+ /** Runtime schema for one current feedback item. */
13
+ export declare const messageFeedbackItemSchema: z.ZodType<MessageFeedbackItem>;
14
+ /** Persisted Session fields that fence a sidecar row to one log lifecycle. */
15
+ export declare const messageFeedbackSessionIdentitySchema: z.ZodObject<{
16
+ createdAt: z.ZodNumber;
17
+ cwd: z.ZodOptional<z.ZodString>;
18
+ }, z.core.$strip>;
19
+ /** Persisted lifecycle identity inferred from its durable schema. */
20
+ export type MessageFeedbackSessionIdentity = z.infer<typeof messageFeedbackSessionIdentitySchema>;
21
+ /**
22
+ * One whole-Session sidecar. Duplicate message ids would make item lookup
23
+ * ambiguous; duplicate versions would break their independent identity.
24
+ */
25
+ export declare const messageFeedbackRowSchema: z.ZodObject<{
26
+ session: z.ZodObject<{
27
+ createdAt: z.ZodNumber;
28
+ cwd: z.ZodOptional<z.ZodString>;
29
+ }, z.core.$strip>;
30
+ items: z.ZodArray<z.ZodType<MessageFeedbackItem, unknown, z.core.$ZodTypeInternals<MessageFeedbackItem, unknown>>>;
31
+ }, z.core.$strip>;
32
+ /** Durable sidecar row inferred from {@link messageFeedbackRowSchema}. */
33
+ export type MessageFeedbackRow = z.infer<typeof messageFeedbackRowSchema>;
34
+ /** One lifecycle-bound sidecar record per Session id. */
35
+ export declare const messageFeedbackDomainSpec: {
36
+ name: string;
37
+ version: number;
38
+ tables: {
39
+ sessions: import("@deepseek-ai/dsh-storage-domain").DomainTableSpec<SessionId, {
40
+ session: {
41
+ createdAt: number;
42
+ cwd?: string | undefined;
43
+ };
44
+ items: MessageFeedbackItem[];
45
+ }>;
46
+ };
47
+ };
48
+ //# sourceMappingURL=spec.d.ts.map
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Durable storage-domain declaration for lifecycle-bound message feedback.
3
+ * @module @deepseek-ai/dsh-message-feedback/src/spec
4
+ */
5
+ import { z } from 'zod';
6
+ import { defineDomain, domainTable } from '@deepseek-ai/dsh-storage-domain';
7
+ const nonNegativeSafeInteger = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
8
+ /** Runtime schema for the closed rating vocabulary. */
9
+ export const messageFeedbackRatingSchema = z.union([
10
+ z.literal('positive'),
11
+ z.literal('negative'),
12
+ ]);
13
+ /** Runtime schema for one opaque item version stored on disk. */
14
+ export const messageFeedbackVersionSchema = z.uuid()
15
+ .transform(value => value);
16
+ /** Runtime schema for one current feedback item. */
17
+ // Zod infers transformed branded fields structurally, so it cannot name the
18
+ // public interface even though every branded output is created below.
19
+ export const messageFeedbackItemSchema = z.object({
20
+ messageId: z.string().min(1).transform(value => value),
21
+ rating: messageFeedbackRatingSchema,
22
+ note: z.string().refine(note => note.trim().length > 0, {
23
+ message: 'message feedback note must contain a non-whitespace character',
24
+ }).optional(),
25
+ version: messageFeedbackVersionSchema,
26
+ createdAt: nonNegativeSafeInteger,
27
+ updatedAt: nonNegativeSafeInteger,
28
+ }).refine(item => item.updatedAt >= item.createdAt, {
29
+ path: ['updatedAt'],
30
+ message: 'message feedback updatedAt must not precede createdAt',
31
+ });
32
+ /** Persisted Session fields that fence a sidecar row to one log lifecycle. */
33
+ export const messageFeedbackSessionIdentitySchema = z.object({
34
+ createdAt: nonNegativeSafeInteger,
35
+ cwd: z.string().optional(),
36
+ });
37
+ /**
38
+ * One whole-Session sidecar. Duplicate message ids would make item lookup
39
+ * ambiguous; duplicate versions would break their independent identity.
40
+ */
41
+ export const messageFeedbackRowSchema = z.object({
42
+ session: messageFeedbackSessionIdentitySchema,
43
+ items: z.array(messageFeedbackItemSchema),
44
+ }).superRefine((row, ctx) => {
45
+ const messageIds = new Set();
46
+ const versions = new Set();
47
+ row.items.forEach((item, index) => {
48
+ if (messageIds.has(item.messageId)) {
49
+ ctx.addIssue({
50
+ code: 'custom',
51
+ path: ['items', index, 'messageId'],
52
+ message: `duplicate message feedback id '${item.messageId}'`,
53
+ });
54
+ }
55
+ messageIds.add(item.messageId);
56
+ if (versions.has(item.version)) {
57
+ ctx.addIssue({
58
+ code: 'custom',
59
+ path: ['items', index, 'version'],
60
+ message: `duplicate message feedback version '${item.version}'`,
61
+ });
62
+ }
63
+ versions.add(item.version);
64
+ });
65
+ });
66
+ /** One lifecycle-bound sidecar record per Session id. */
67
+ export const messageFeedbackDomainSpec = defineDomain({
68
+ name: 'message_feedback',
69
+ version: 0,
70
+ tables: {
71
+ sessions: domainTable(messageFeedbackRowSchema),
72
+ },
73
+ });
74
+ //# sourceMappingURL=spec.js.map
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Public request, value, and failure vocabulary for per-message feedback.
3
+ * This module contains types only so generated Remote clients can consume it
4
+ * without importing Host runtime code.
5
+ * @module @deepseek-ai/dsh-message-feedback/types
6
+ */
7
+ import type { Branded } from '@deepseek-ai/dsh-brand';
8
+ import type { MessageId } from '@deepseek-ai/dsh-llm/brand';
9
+ import type { SessionId } from '@deepseek-ai/dsh-session/types';
10
+ /** Opaque compare-and-set token for one exact feedback item revision. */
11
+ export type MessageFeedbackVersion = Branded<'MessageFeedbackVersion'>;
12
+ /** The human's overall judgment of one assistant message. */
13
+ export type MessageFeedbackRating = 'positive' | 'negative';
14
+ /** One current feedback value and its opaque mutation token. */
15
+ export interface MessageFeedbackItem {
16
+ /** Stable identity of the assistant message inside the owning Session. */
17
+ readonly messageId: MessageId;
18
+ /** Overall positive or negative judgment. */
19
+ readonly rating: MessageFeedbackRating;
20
+ /** Optional explanation, preserved verbatim after validation. */
21
+ readonly note?: string;
22
+ /** Equality-only token replaced by every material create or update. */
23
+ readonly version: MessageFeedbackVersion;
24
+ /** Host-assigned creation time in Unix epoch milliseconds. */
25
+ readonly createdAt: number;
26
+ /** Host-assigned time of the most recent material update. */
27
+ readonly updatedAt: number;
28
+ }
29
+ /** Read all message feedback belonging to one persisted Session lifecycle. */
30
+ export interface MessageFeedbackListRequest {
31
+ /** Persisted Session whose sidecar should be read. */
32
+ readonly sessionId: SessionId;
33
+ }
34
+ /** Current feedback values for one Session, in first-creation order. */
35
+ export interface MessageFeedbackListValue {
36
+ /** Fresh immutable item snapshots. */
37
+ readonly items: readonly MessageFeedbackItem[];
38
+ }
39
+ /** Create or replace feedback for one assistant message. */
40
+ export interface MessageFeedbackPutRequest {
41
+ /** Persisted Session that owns the target message. */
42
+ readonly sessionId: SessionId;
43
+ /** Target assistant-message identity. */
44
+ readonly messageId: MessageId;
45
+ /** Desired overall judgment. */
46
+ readonly rating: MessageFeedbackRating;
47
+ /** Optional non-blank explanation. */
48
+ readonly note?: string;
49
+ /** Observed item version, or `null` to require that no item exists. */
50
+ readonly ifVersion: MessageFeedbackVersion | null;
51
+ }
52
+ /** Delete feedback for one message after observing its current version. */
53
+ export interface MessageFeedbackDeleteRequest {
54
+ /** Persisted Session that owns the sidecar. */
55
+ readonly sessionId: SessionId;
56
+ /** Message whose feedback should be absent after this operation. */
57
+ readonly messageId: MessageId;
58
+ /** Observed item version; ignored when the item is already absent. */
59
+ readonly ifVersion: MessageFeedbackVersion;
60
+ }
61
+ /** Idempotent deletion acknowledgement. */
62
+ export interface MessageFeedbackDeleteValue {
63
+ /** Stable postcondition shared by the first deletion and every retry. */
64
+ readonly absent: true;
65
+ }
66
+ /** No persisted Session header exists for the requested id. */
67
+ export interface MessageFeedbackSessionNotFound {
68
+ readonly code: 'session-not-found';
69
+ readonly sessionId: SessionId;
70
+ }
71
+ /** The id does not name a derived, append-origin assistant message. */
72
+ export interface MessageFeedbackTargetNotFound {
73
+ readonly code: 'target-not-found';
74
+ readonly sessionId: SessionId;
75
+ readonly messageId: MessageId;
76
+ }
77
+ /** A material mutation did not match the addressed item's current version. */
78
+ export interface MessageFeedbackVersionConflict {
79
+ readonly code: 'version-conflict';
80
+ /** Authoritative current item, or `null` when it does not exist. */
81
+ readonly current: MessageFeedbackItem | null;
82
+ }
83
+ /** A supplied note contains no non-whitespace character. */
84
+ export interface MessageFeedbackNoteBlank {
85
+ readonly code: 'note-blank';
86
+ }
87
+ /** A supplied note exceeds the configured UTF-8 byte limit. */
88
+ export interface MessageFeedbackNoteTooLarge {
89
+ readonly code: 'note-too-large';
90
+ readonly maxBytes: number;
91
+ readonly actualBytes: number;
92
+ }
93
+ /** Failures shared by the public message-feedback operations. */
94
+ export type MessageFeedbackFailure = MessageFeedbackSessionNotFound | MessageFeedbackTargetNotFound | MessageFeedbackVersionConflict | MessageFeedbackNoteBlank | MessageFeedbackNoteTooLarge;
95
+ /** Successful public operation result. */
96
+ export interface MessageFeedbackSuccess<T> {
97
+ readonly ok: true;
98
+ readonly value: T;
99
+ }
100
+ /** Rejected public operation result with a stable business failure. */
101
+ export interface MessageFeedbackRejected<E extends MessageFeedbackFailure> {
102
+ readonly ok: false;
103
+ readonly error: E;
104
+ }
105
+ /** Result returned by the message-feedback `list` operation. */
106
+ export type MessageFeedbackListResult = MessageFeedbackSuccess<MessageFeedbackListValue> | MessageFeedbackRejected<MessageFeedbackSessionNotFound>;
107
+ /** Result returned by the message-feedback `put` operation. */
108
+ export type MessageFeedbackPutResult = MessageFeedbackSuccess<MessageFeedbackItem> | MessageFeedbackRejected<MessageFeedbackSessionNotFound | MessageFeedbackTargetNotFound | MessageFeedbackVersionConflict | MessageFeedbackNoteBlank | MessageFeedbackNoteTooLarge>;
109
+ /** Result returned by the message-feedback `delete` operation. */
110
+ export type MessageFeedbackDeleteResult = MessageFeedbackSuccess<MessageFeedbackDeleteValue> | MessageFeedbackRejected<MessageFeedbackSessionNotFound | MessageFeedbackVersionConflict>;
111
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Public request, value, and failure vocabulary for per-message feedback.
3
+ * This module contains types only so generated Remote clients can consume it
4
+ * without importing Host runtime code.
5
+ * @module @deepseek-ai/dsh-message-feedback/types
6
+ */
7
+ export {};
8
+ //# sourceMappingURL=types.js.map