@deepseek-ai/dsh-message-feedback 0.1.2-alpha.5 → 0.1.3-alpha.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.
@@ -1,14 +1,13 @@
1
1
  /**
2
- * Durable, lifecycle-bound feedback for finalized assistant messages.
2
+ * Canonical Session-log feedback for finalized assistant messages.
3
3
  * @module @deepseek-ai/dsh-message-feedback
4
4
  */
5
5
  import { Context, Service } from '@deepseek-ai/cordis';
6
6
  import s from '@deepseek-ai/schemastery';
7
+ import type { SessionInspection } from '@deepseek-ai/dsh-session-persistence';
7
8
  import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
8
9
  import type { MessageFeedbackDeleteRequest, MessageFeedbackDeleteResult, MessageFeedbackListRequest, MessageFeedbackListResult, MessageFeedbackPutRequest, MessageFeedbackPutResult } from './types.ts';
9
10
  export type * from './types.ts';
10
- export { messageFeedbackDomainSpec, messageFeedbackItemSchema, messageFeedbackRatingSchema, messageFeedbackRowSchema, messageFeedbackSessionIdentitySchema, messageFeedbackVersionSchema, } from './spec.ts';
11
- export type { MessageFeedbackRow, MessageFeedbackSessionIdentity } from './spec.ts';
12
11
  /** Required deployment policy for optional notes. */
13
12
  export interface Config {
14
13
  /** Maximum UTF-8 byte length accepted for one note. */
@@ -18,71 +17,57 @@ declare module '@deepseek-ai/cordis' {
18
17
  interface Context {
19
18
  messageFeedback: MessageFeedbackService;
20
19
  }
20
+ interface Events {
21
+ /**
22
+ * Observe a durable cold feedback mutation without publishing a live Session.
23
+ * Observers run before write ownership is released and must not await
24
+ * another message-feedback operation for this Session. The payload is borrowed
25
+ * read-only; deep-clone it before transferring ownership (for example, to Session.fromRestore).
26
+ * @param inspection - committed canonical prefix, including the feedback as its last event.
27
+ * @mode parallel
28
+ */
29
+ 'feedback/committed'(inspection: SessionInspection): void;
30
+ }
21
31
  }
22
- /**
23
- * Storage-domain sidecar service. It inspects persisted Session history and
24
- * never creates or resumes an Agent or Session.
25
- */
32
+ /** Session-log service; cold operations never construct a Session or Agent. */
26
33
  export declare class MessageFeedbackService extends TypertRemoteService {
27
34
  static inject: string[];
28
35
  /** Loader validation for the required note-size policy. */
29
36
  static Config: s<Config>;
30
37
  private readonly maxNoteBytes;
31
- private table?;
32
38
  private readonly operationTails;
33
39
  private mutationAdmissionOpen;
34
40
  /**
35
- * @param ctx - Host context carrying persistence and the storage-domain form.
41
+ * @param ctx - Host context carrying Session persistence and live owners.
36
42
  * @param config - Required note-size policy.
37
43
  */
38
44
  constructor(ctx: Context, config: Config);
39
- /** Open and own the one message-feedback sidecar domain. */
40
- protected [Service.init](): Promise<void>;
45
+ protected [Service.init](): void;
41
46
  /**
42
- * Read feedback belonging to the current persisted Session lifecycle.
43
- * A stale row from a reused Session id is invisible.
44
- * @param request - Session identity to inspect and list.
45
- * @returns current immutable items or `session-not-found`.
47
+ * Read current feedback from the canonical log.
48
+ * @param request - Session to inspect.
49
+ * @returns immutable items or a definite persistence miss.
46
50
  */
47
51
  list(request: MessageFeedbackListRequest): Promise<MessageFeedbackListResult>;
48
52
  /**
49
- * Create or replace feedback for one derived append-origin assistant
50
- * message. Every request must match the addressed item's current version;
51
- * a matching no-op returns the stored item without changing its revision.
52
- * @param request - target, desired value, and observed item version.
53
- * @returns the committed item or an explicit business failure.
53
+ * Create or replace feedback after checking its current version.
54
+ * Matching no-ops retain the version and append no event.
55
+ * @param request - Target, desired value, and observed item version.
56
+ * @returns the durable item or an explicit business failure.
54
57
  */
55
58
  put(request: MessageFeedbackPutRequest): Promise<MessageFeedbackPutResult>;
56
59
  /**
57
- * Delete one feedback item. Absence is successful regardless of the
58
- * supplied version; an existing item requires an exact version match.
60
+ * Delete one item after checking its version; absence succeeds without an event.
59
61
  * @param request - Session, message, and observed item version.
60
- * @returns the stable absent postcondition, or an explicit failure.
62
+ * @returns the stable absent postcondition or an explicit failure.
61
63
  */
62
64
  delete(request: MessageFeedbackDeleteRequest): Promise<MessageFeedbackDeleteResult>;
63
- /**
64
- * Resolve a live owner directly; otherwise use the storage catalog as the
65
- * existence authority before inspecting the log. Inspection failures for a
66
- * catalogued Session remain infrastructure failures rather than being
67
- * guessed into the business `session-not-found` branch.
68
- */
69
- private inspectSession;
70
- /** Require the exact finalized append-origin assistant message projection. */
71
- private hasFeedbackTarget;
72
- /**
73
- * Put the target log prefix behind a durability barrier before its sidecar.
74
- * A live owner flushes through the SessionStore's canonical checkpoint; a
75
- * cold owner is re-read from the physical durable prefix.
76
- */
77
- private ensureTargetDurable;
78
- /** Validate optional-note semantics and the configured complete UTF-8 byte bound. */
65
+ /** Hold cold write ownership across read/compare/append; use live owners directly. */
66
+ private withSession;
79
67
  private resolveNote;
80
- /** Return the authoritative item needed to reconcile one failed comparison. */
81
68
  private versionConflict;
82
- /** Queue a complete read/compare/write mutation behind this Session's prior mutation. */
69
+ /** Serialize complete operations and drain their handles before disposal. */
83
70
  private enqueue;
84
- /** Resolve the initialized durable table or fail a broken service lifecycle. */
85
- private requireTable;
86
71
  }
87
72
  export default MessageFeedbackService;
88
73
  //# sourceMappingURL=index.d.ts.map
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Durable, lifecycle-bound feedback for finalized assistant messages.
2
+ * Canonical Session-log feedback for finalized assistant messages.
3
3
  * @module @deepseek-ai/dsh-message-feedback
4
4
  */
5
5
  var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
@@ -38,77 +38,57 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
38
38
  };
39
39
  import { Buffer } from 'node:buffer';
40
40
  import { randomUUID } from 'node:crypto';
41
+ import { isDeepStrictEqual } from 'node:util';
41
42
  import { Service } from '@deepseek-ai/cordis';
42
43
  import s from '@deepseek-ai/schemastery';
43
- import { SessionLogOffset } from '@deepseek-ai/dsh-session';
44
+ import { z } from 'zod';
45
+ import { SessionSeq } from '@deepseek-ai/dsh-session/types';
44
46
  import { deriveEventMessage, isAppendSurfaceEvent } from '@deepseek-ai/dsh-session/surface';
45
47
  import { TypertRemoteService, Remote } from '@deepseek-ai/dsh-typert-protocol';
46
- import { messageFeedbackDomainSpec } from "./spec.js";
47
- export { messageFeedbackDomainSpec, messageFeedbackItemSchema, messageFeedbackRatingSchema, messageFeedbackRowSchema, messageFeedbackSessionIdentitySchema, messageFeedbackVersionSchema, } from "./spec.js";
48
- /** Immutable empty list reused only as an input to caller-owned copying. */
49
- const EMPTY_ITEMS = Object.freeze([]);
50
- /** Validate the one deployment-varying limit at the configuration boundary. */
51
- function resolveMaxNoteBytes(value) {
52
- if (!Number.isSafeInteger(value) || value < 1) {
53
- throw new TypeError(`message-feedback: maxNoteBytes must be a positive safe integer, got ${String(value)}`);
54
- }
55
- return value;
56
- }
57
- /** Copy and freeze one item before it crosses the service boundary. */
48
+ const timestamp = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
49
+ const itemSchema = z.object({
50
+ messageId: z.string().min(1),
51
+ rating: z.enum(['positive', 'negative']),
52
+ note: z.string().refine(note => note.trim().length > 0).optional(),
53
+ version: z.uuid(),
54
+ createdAt: timestamp,
55
+ updatedAt: timestamp,
56
+ }).refine(item => item.updatedAt >= item.createdAt);
57
+ const putSchema = z.object({ sessionId: z.string().min(1), item: itemSchema });
58
+ const deleteSchema = z.object({ sessionId: z.string().min(1), messageId: z.string().min(1) });
59
+ /** Return a caller-owned immutable value, detached from the log. */
58
60
  function snapshotItem(item) {
59
- return Object.freeze({
60
- messageId: item.messageId,
61
- rating: item.rating,
62
- ...(item.note === undefined ? {} : { note: item.note }),
63
- version: item.version,
64
- createdAt: item.createdAt,
65
- updatedAt: item.updatedAt,
66
- });
67
- }
68
- /** Copy and freeze a list response. */
69
- function snapshotList(items) {
70
- return Object.freeze({ items: Object.freeze(items.map(snapshotItem)) });
61
+ return Object.freeze({ ...item });
71
62
  }
72
- /** Build a frozen success branch. */
73
63
  function success(value) {
74
64
  return Object.freeze({ ok: true, value });
75
65
  }
76
- /** Build a frozen business-failure branch. */
77
66
  function rejected(error) {
78
67
  return Object.freeze({ ok: false, error: Object.freeze(error) });
79
68
  }
80
- /** Project the Session fields that distinguish one persisted log lifecycle. */
81
- function identityOf(header) {
82
- return Object.freeze({
83
- createdAt: header.createdAt,
84
- ...(header.cwd === undefined ? {} : { cwd: header.cwd }),
85
- });
86
- }
87
- /** Whether a stored row belongs to the inspected Session lifecycle. */
88
- function sameIdentity(row, header) {
89
- return row.session.createdAt === header.createdAt && row.session.cwd === header.cwd;
90
- }
91
- /** Whether two observations name the same persisted Session lifecycle. */
92
- function sameHeaderIdentity(left, right) {
93
- return left.id === right.id && left.createdAt === right.createdAt && left.cwd === right.cwd;
94
- }
95
- /** Freeze the replacement row so storage-domain never exposes mutable aliases. */
96
- function rowSnapshot(session, items) {
97
- const copiedItems = items.map(snapshotItem);
98
- Object.freeze(copiedItems);
99
- return Object.freeze({
100
- session,
101
- items: copiedItems,
102
- });
103
- }
104
- /** Generate an opaque equality token for one material mutation. */
105
- function nextVersion() {
106
- return randomUUID();
69
+ /** Validate persisted payloads before deriving current, Session-owned feedback. */
70
+ function currentItems(sessionId, events) {
71
+ const items = new Map();
72
+ for (const event of events) {
73
+ switch (event.type) {
74
+ case 'feedback/message-put':
75
+ putSchema.parse(event.data);
76
+ if (event.data.sessionId === sessionId)
77
+ items.set(event.data.item.messageId, event.data.item);
78
+ break;
79
+ case 'feedback/message-delete':
80
+ deleteSchema.parse(event.data);
81
+ if (event.data.sessionId === sessionId)
82
+ items.delete(event.data.messageId);
83
+ break;
84
+ default:
85
+ // Other plugins' events do not change message feedback.
86
+ break;
87
+ }
88
+ }
89
+ return [...items.values()];
107
90
  }
108
- /**
109
- * Storage-domain sidecar service. It inspects persisted Session history and
110
- * never creates or resumes an Agent or Session.
111
- */
91
+ /** Session-log service; cold operations never construct a Session or Agent. */
112
92
  let MessageFeedbackService = (() => {
113
93
  let _classSuper = TypertRemoteService;
114
94
  let _instanceExtraInitializers = [];
@@ -126,177 +106,154 @@ let MessageFeedbackService = (() => {
126
106
  __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);
127
107
  if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
128
108
  }
129
- static inject = ['storageDomain', 'sessionPersistence', 'sessions'];
109
+ static inject = ['sessionPersistence', 'sessions'];
130
110
  /** Loader validation for the required note-size policy. */
131
111
  static Config = s.object({
132
112
  maxNoteBytes: s.number().step(1).min(1).required(),
133
113
  });
134
114
  maxNoteBytes = __runInitializers(this, _instanceExtraInitializers);
135
- table;
136
115
  operationTails = new Map();
137
116
  mutationAdmissionOpen = true;
138
117
  /**
139
- * @param ctx - Host context carrying persistence and the storage-domain form.
118
+ * @param ctx - Host context carrying Session persistence and live owners.
140
119
  * @param config - Required note-size policy.
141
120
  */
142
121
  constructor(ctx, config) {
143
122
  super(ctx, 'messageFeedback');
144
- this.maxNoteBytes = resolveMaxNoteBytes(config.maxNoteBytes);
123
+ if (!Number.isSafeInteger(config.maxNoteBytes) || config.maxNoteBytes < 1) {
124
+ throw new TypeError('message-feedback: maxNoteBytes must be a positive safe integer');
125
+ }
126
+ this.maxNoteBytes = config.maxNoteBytes;
145
127
  }
146
- /** Open and own the one message-feedback sidecar domain. */
147
- async [Service.init]() {
148
- const domain = await this.ctx.storageDomain.open(messageFeedbackDomainSpec);
128
+ [Service.init]() {
149
129
  this.ctx.effect(() => async () => {
150
130
  this.mutationAdmissionOpen = false;
151
131
  await Promise.all(this.operationTails.values());
152
- await domain.close();
153
- }, 'message-feedback.domainClose');
154
- this.table = domain.table('sessions');
132
+ }, 'message-feedback.drain');
155
133
  }
156
134
  /**
157
- * Read feedback belonging to the current persisted Session lifecycle.
158
- * A stale row from a reused Session id is invisible.
159
- * @param request - Session identity to inspect and list.
160
- * @returns current immutable items or `session-not-found`.
135
+ * Read current feedback from the canonical log.
136
+ * @param request - Session to inspect.
137
+ * @returns immutable items or a definite persistence miss.
161
138
  */
162
- async list(request) {
163
- const known = await this.inspectSession(request.sessionId);
164
- if (!known.ok)
165
- return known;
166
- const row = this.requireTable().get(request.sessionId);
167
- const items = row !== undefined && sameIdentity(row, known.value.meta) ? row.items : EMPTY_ITEMS;
168
- return success(snapshotList(items));
139
+ list(request) {
140
+ return this.enqueue(request.sessionId, () => this.withSession(request.sessionId, false, events => success(Object.freeze({ items: Object.freeze(currentItems(request.sessionId, events).map(snapshotItem)) }))));
169
141
  }
170
142
  /**
171
- * Create or replace feedback for one derived append-origin assistant
172
- * message. Every request must match the addressed item's current version;
173
- * a matching no-op returns the stored item without changing its revision.
174
- * @param request - target, desired value, and observed item version.
175
- * @returns the committed item or an explicit business failure.
143
+ * Create or replace feedback after checking its current version.
144
+ * Matching no-ops retain the version and append no event.
145
+ * @param request - Target, desired value, and observed item version.
146
+ * @returns the durable item or an explicit business failure.
176
147
  */
177
148
  put(request) {
178
149
  const note = this.resolveNote(request.note);
179
150
  if (!note.ok)
180
151
  return Promise.resolve(note);
181
- return this.enqueue(request.sessionId, async () => {
182
- const known = await this.inspectSession(request.sessionId);
183
- if (!known.ok)
184
- return known;
185
- if (!this.hasFeedbackTarget(known.value, request.messageId)) {
186
- return rejected({
187
- code: 'target-not-found',
188
- sessionId: request.sessionId,
189
- messageId: request.messageId,
190
- });
191
- }
192
- const durable = await this.ensureTargetDurable(known.value);
193
- if (!sameHeaderIdentity(durable.meta, known.value.meta)
194
- || !this.hasFeedbackTarget(durable, request.messageId)) {
195
- return rejected({
196
- code: 'target-not-found',
197
- sessionId: request.sessionId,
198
- messageId: request.messageId,
199
- });
152
+ return this.enqueue(request.sessionId, () => this.withSession(request.sessionId, true, async (events, append) => {
153
+ const items = currentItems(request.sessionId, events);
154
+ if (!events.some(event => event.type === 'assistant/message'
155
+ && isAppendSurfaceEvent(event)
156
+ && deriveEventMessage(event)?.id === request.messageId)) {
157
+ return rejected({ code: 'target-not-found', sessionId: request.sessionId, messageId: request.messageId });
200
158
  }
201
- const table = this.requireTable();
202
- const stored = table.get(request.sessionId);
203
- const current = stored !== undefined && sameIdentity(stored, durable.meta) ? stored : undefined;
204
- const items = current?.items ?? EMPTY_ITEMS;
205
- const index = items.findIndex(item => item.messageId === request.messageId);
206
- const existing = items[index];
159
+ const existing = items.find(item => item.messageId === request.messageId);
207
160
  if (request.ifVersion !== (existing?.version ?? null)) {
208
161
  return rejected(this.versionConflict(existing ?? null));
209
162
  }
210
- if (existing !== undefined
211
- && existing.rating === request.rating
212
- && existing.note === note.value) {
163
+ if (existing !== undefined && existing.rating === request.rating && existing.note === note.value) {
164
+ await append();
213
165
  return success(snapshotItem(existing));
214
166
  }
215
167
  const now = Date.now();
216
- const item = snapshotItem({
168
+ const item = {
217
169
  messageId: request.messageId,
218
170
  rating: request.rating,
219
171
  ...(note.value === undefined ? {} : { note: note.value }),
220
- version: nextVersion(),
172
+ version: randomUUID(),
221
173
  createdAt: existing?.createdAt ?? now,
222
174
  updatedAt: existing === undefined ? now : Math.max(now, existing.updatedAt),
223
- });
224
- const nextItems = [...items];
225
- if (index === -1)
226
- nextItems.push(item);
227
- else
228
- nextItems[index] = item;
229
- await table.put(request.sessionId, rowSnapshot(identityOf(durable.meta), nextItems));
175
+ };
176
+ await append({ type: 'feedback/message-put', data: { sessionId: request.sessionId, item } });
230
177
  return success(snapshotItem(item));
231
- });
178
+ }));
232
179
  }
233
180
  /**
234
- * Delete one feedback item. Absence is successful regardless of the
235
- * supplied version; an existing item requires an exact version match.
181
+ * Delete one item after checking its version; absence succeeds without an event.
236
182
  * @param request - Session, message, and observed item version.
237
- * @returns the stable absent postcondition, or an explicit failure.
183
+ * @returns the stable absent postcondition or an explicit failure.
238
184
  */
239
185
  delete(request) {
240
- return this.enqueue(request.sessionId, async () => {
241
- const known = await this.inspectSession(request.sessionId);
242
- if (!known.ok)
243
- return known;
244
- const table = this.requireTable();
245
- const stored = table.get(request.sessionId);
246
- const current = stored !== undefined && sameIdentity(stored, known.value.meta) ? stored : undefined;
247
- const items = current?.items ?? EMPTY_ITEMS;
248
- const existing = items.find(item => item.messageId === request.messageId);
249
- if (existing === undefined) {
250
- return success(Object.freeze({ absent: true }));
186
+ return this.enqueue(request.sessionId, () => this.withSession(request.sessionId, true, async (events, append) => {
187
+ const existing = currentItems(request.sessionId, events).find(item => item.messageId === request.messageId);
188
+ if (existing !== undefined) {
189
+ if (request.ifVersion !== existing.version)
190
+ return rejected(this.versionConflict(existing));
191
+ await append({ type: 'feedback/message-delete', data: { sessionId: request.sessionId, messageId: request.messageId } });
251
192
  }
252
- if (request.ifVersion !== existing.version) {
253
- return rejected(this.versionConflict(existing));
193
+ else {
194
+ await append();
254
195
  }
255
- await table.put(request.sessionId, rowSnapshot(identityOf(known.value.meta), items.filter(item => item !== existing)));
256
196
  return success(Object.freeze({ absent: true }));
257
- });
197
+ }));
258
198
  }
259
- /**
260
- * Resolve a live owner directly; otherwise use the storage catalog as the
261
- * existence authority before inspecting the log. Inspection failures for a
262
- * catalogued Session remain infrastructure failures rather than being
263
- * guessed into the business `session-not-found` branch.
264
- */
265
- async inspectSession(sessionId) {
266
- if (this.ctx.sessions.get(sessionId) === undefined) {
267
- const snapshots = await this.ctx.sessionPersistence.listSnapshots();
268
- if (!snapshots.some(snapshot => snapshot.header.id === sessionId)
269
- && this.ctx.sessions.get(sessionId) === undefined) {
270
- return rejected({ code: 'session-not-found', sessionId });
271
- }
199
+ /** Hold cold write ownership across read/compare/append; use live owners directly. */
200
+ async withSession(sessionId, write, operation) {
201
+ if (this.ctx.sessions.get(sessionId) === undefined
202
+ && await this.ctx.sessionPersistence.stat(sessionId) === undefined
203
+ && this.ctx.sessions.get(sessionId) === undefined) {
204
+ return rejected({ code: 'session-not-found', sessionId });
272
205
  }
273
- return success(await this.ctx.sessionPersistence.inspect(sessionId));
274
- }
275
- /** Require the exact finalized append-origin assistant message projection. */
276
- hasFeedbackTarget(inspection, messageId) {
277
- return inspection.events.some((event) => {
278
- if (event.type !== 'assistant/message' || !isAppendSurfaceEvent(event))
279
- return false;
280
- const message = deriveEventMessage(event);
281
- return message?.role === 'assistant' && message.id === messageId;
282
- });
283
- }
284
- /**
285
- * Put the target log prefix behind a durability barrier before its sidecar.
286
- * A live owner flushes through the SessionStore's canonical checkpoint; a
287
- * cold owner is re-read from the physical durable prefix.
288
- */
289
- async ensureTargetDurable(inspection) {
290
- const live = this.ctx.sessions.get(inspection.meta.id);
291
- if (live !== undefined && sameHeaderIdentity(live.header, inspection.meta)) {
292
- if (!(await this.ctx.sessions.flush(live))) {
293
- throw new Error(`message-feedback: no durability listener participated for live session '${inspection.meta.id}'`);
294
- }
295
- return await this.ctx.sessionPersistence.readFrom(inspection.meta.id, SessionLogOffset(0));
206
+ const live = this.ctx.sessions.get(sessionId);
207
+ if (live !== undefined) {
208
+ return operation(live.snapshotEvents(), async (event) => {
209
+ if (event !== undefined) {
210
+ live.append(event.type, event.data);
211
+ }
212
+ const last = live.snapshotEvents().at(-1);
213
+ if (!(await this.ctx.sessions.flush(live))) {
214
+ throw new Error(`message-feedback: no durability listener participated for live session '${sessionId}'`);
215
+ }
216
+ // Listener participation alone does not prove this Session has a persistence writer.
217
+ const handle = await this.ctx.sessionPersistence.open(sessionId, 'read');
218
+ try {
219
+ const { events: stored } = await handle.read(last?.seq ?? 0, 1);
220
+ if (!isDeepStrictEqual([handle.header.id, handle.header.createdAt, handle.header.cwd], [live.header.id, live.header.createdAt, live.header.cwd])
221
+ || (last !== undefined && !isDeepStrictEqual(stored[0], last))) {
222
+ throw new Error(`message-feedback: feedback prefix is not durable for live session '${sessionId}'`);
223
+ }
224
+ }
225
+ finally {
226
+ await handle.close();
227
+ }
228
+ });
229
+ }
230
+ const handle = await this.ctx.sessionPersistence.open(sessionId, write ? 'write' : 'read');
231
+ try {
232
+ const { events } = await handle.read();
233
+ return await operation(events, async (event) => {
234
+ const entry = event === undefined ? undefined
235
+ : { ...event, seq: SessionSeq(events.length), time: Date.now() };
236
+ if (entry !== undefined)
237
+ await handle.append([entry]);
238
+ await handle.flush();
239
+ if (entry !== undefined) {
240
+ try {
241
+ await this.ctx.parallel('feedback/committed', {
242
+ meta: handle.header,
243
+ inheritedEventCount: handle.inheritedEventCount,
244
+ events: [...events, entry],
245
+ });
246
+ }
247
+ catch (error) {
248
+ this.ctx.logger.warn('message-feedback: committed feedback observer failed', error);
249
+ }
250
+ }
251
+ });
252
+ }
253
+ finally {
254
+ await handle.close();
296
255
  }
297
- return await this.ctx.sessionPersistence.readFrom(inspection.meta.id, SessionLogOffset(0));
298
256
  }
299
- /** Validate optional-note semantics and the configured complete UTF-8 byte bound. */
300
257
  resolveNote(note) {
301
258
  if (note === undefined)
302
259
  return success(undefined);
@@ -308,18 +265,13 @@ let MessageFeedbackService = (() => {
308
265
  }
309
266
  return success(note);
310
267
  }
311
- /** Return the authoritative item needed to reconcile one failed comparison. */
312
268
  versionConflict(current) {
313
- return {
314
- code: 'version-conflict',
315
- current: current === null ? null : snapshotItem(current),
316
- };
269
+ return { code: 'version-conflict', current: current === null ? null : snapshotItem(current) };
317
270
  }
318
- /** Queue a complete read/compare/write mutation behind this Session's prior mutation. */
271
+ /** Serialize complete operations and drain their handles before disposal. */
319
272
  enqueue(sessionId, operation) {
320
- if (!this.mutationAdmissionOpen) {
273
+ if (!this.mutationAdmissionOpen)
321
274
  return Promise.reject(new Error('message-feedback: service is disposing'));
322
- }
323
275
  const previous = this.operationTails.get(sessionId) ?? Promise.resolve();
324
276
  const result = previous.then(operation);
325
277
  const tail = result.then(() => undefined, () => undefined);
@@ -329,13 +281,6 @@ let MessageFeedbackService = (() => {
329
281
  this.operationTails.delete(sessionId);
330
282
  });
331
283
  }
332
- /** Resolve the initialized durable table or fail a broken service lifecycle. */
333
- requireTable() {
334
- if (this.table === undefined) {
335
- throw new Error('message-feedback: durable domain is not initialized');
336
- }
337
- return this.table;
338
- }
339
284
  };
340
285
  })();
341
286
  export { MessageFeedbackService };
@@ -26,9 +26,31 @@ export interface MessageFeedbackItem {
26
26
  /** Host-assigned time of the most recent material update. */
27
27
  readonly updatedAt: number;
28
28
  }
29
+ /** A material creation or edit, retaining its complete current value. */
30
+ export interface MessageFeedbackPut {
31
+ /** Owning Session; inherited feedback in a fork belongs to its parent. */
32
+ readonly sessionId: SessionId;
33
+ /** Value after this mutation, including the original creation time. */
34
+ readonly item: MessageFeedbackItem;
35
+ }
36
+ /** A material deletion of one current feedback item. */
37
+ export interface MessageFeedbackDelete {
38
+ /** Session that owns the deleted feedback. */
39
+ readonly sessionId: SessionId;
40
+ /** Message whose feedback was removed. */
41
+ readonly messageId: MessageId;
42
+ }
43
+ declare module '@deepseek-ai/dsh-session/types' {
44
+ interface SessionEventMap {
45
+ /** Log-only human feedback; never enters model history. */
46
+ 'feedback/message-put': MessageFeedbackPut;
47
+ /** Log-only deletion; earlier ratings and notes remain in the log. */
48
+ 'feedback/message-delete': MessageFeedbackDelete;
49
+ }
50
+ }
29
51
  /** Read all message feedback belonging to one persisted Session lifecycle. */
30
52
  export interface MessageFeedbackListRequest {
31
- /** Persisted Session whose sidecar should be read. */
53
+ /** Session whose feedback events should be read. */
32
54
  readonly sessionId: SessionId;
33
55
  }
34
56
  /** Current feedback values for one Session, in first-creation order. */
@@ -51,7 +73,7 @@ export interface MessageFeedbackPutRequest {
51
73
  }
52
74
  /** Delete feedback for one message after observing its current version. */
53
75
  export interface MessageFeedbackDeleteRequest {
54
- /** Persisted Session that owns the sidecar. */
76
+ /** Session that owns the feedback. */
55
77
  readonly sessionId: SessionId;
56
78
  /** Message whose feedback should be absent after this operation. */
57
79
  readonly messageId: MessageId;