@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.
- package/README.i18n.yaml +2 -2
- package/README.md +32 -91
- package/README.zh.md +32 -91
- package/lib/index.js +138 -231
- package/lib/typert.host.js +36 -14
- package/lib/typert.remote-client.js +3 -3
- package/lib/types/index.d.ts +28 -43
- package/lib/types/index.js +139 -194
- package/lib/types/types.d.ts +24 -2
- package/package.json +15 -19
- package/lib/types/spec.d.ts +0 -48
- package/lib/types/spec.js +0 -74
package/lib/types/index.d.ts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
|
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
|
-
|
|
40
|
-
protected [Service.init](): Promise<void>;
|
|
45
|
+
protected [Service.init](): void;
|
|
41
46
|
/**
|
|
42
|
-
* Read feedback
|
|
43
|
-
*
|
|
44
|
-
* @
|
|
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
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* @
|
|
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
|
|
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
|
|
62
|
+
* @returns the stable absent postcondition or an explicit failure.
|
|
61
63
|
*/
|
|
62
64
|
delete(request: MessageFeedbackDeleteRequest): Promise<MessageFeedbackDeleteResult>;
|
|
63
|
-
/**
|
|
64
|
-
|
|
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
|
-
/**
|
|
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
|
package/lib/types/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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 {
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
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
|
-
/**
|
|
81
|
-
function
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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 = ['
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
153
|
-
}, 'message-feedback.domainClose');
|
|
154
|
-
this.table = domain.table('sessions');
|
|
132
|
+
}, 'message-feedback.drain');
|
|
155
133
|
}
|
|
156
134
|
/**
|
|
157
|
-
* Read feedback
|
|
158
|
-
*
|
|
159
|
-
* @
|
|
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
|
-
|
|
163
|
-
|
|
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
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
* @
|
|
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
|
|
183
|
-
if (!
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
168
|
+
const item = {
|
|
217
169
|
messageId: request.messageId,
|
|
218
170
|
rating: request.rating,
|
|
219
171
|
...(note.value === undefined ? {} : { note: note.value }),
|
|
220
|
-
version:
|
|
172
|
+
version: randomUUID(),
|
|
221
173
|
createdAt: existing?.createdAt ?? now,
|
|
222
174
|
updatedAt: existing === undefined ? now : Math.max(now, existing.updatedAt),
|
|
223
|
-
}
|
|
224
|
-
|
|
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
|
|
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
|
|
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
|
|
242
|
-
if (
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
253
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
/**
|
|
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 };
|
package/lib/types/types.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
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;
|