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

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 CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/feedback/message-feedback/README.md
5
- README.md: 54c6b92fbb68027a4aedb198948785183661a724
6
- README.zh.md: 29cbee0c1ec2ee810948d895c762d2e6320e9b66
5
+ README.md: b85104abb983eb3b33ab6ddad9328e6f5f3b1fc9
6
+ README.zh.md: 52efef5119854f4a5c3c776a227032b59b680fc5
package/README.md CHANGED
@@ -37,7 +37,7 @@ Message feedback is not Session-log content or a Session projection. It emits no
37
37
 
38
38
  ## Service and Host Remote contract
39
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.
40
+ The same three `MessageFeedbackService` methods are published by `TypertRemoteService` 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
41
 
42
42
  | Method | Request | Success `value` | Rejected `error.code` |
43
43
  |---|---|---|---|
package/README.zh.md CHANGED
@@ -37,7 +37,7 @@ message feedback 不是 Session 日志内容或 Session 投影。它不发出 `f
37
37
 
38
38
  ## 服务与 Host Remote 契约
39
39
 
40
- `GatewayService` 与 `@Remote` 将 `MessageFeedbackService` 的同三个方法发布出去;Host endpoint 名称为 `messageFeedback.list`、`messageFeedback.put` 与 `messageFeedback.delete`。每个方法都返回判别式业务 union:`{ ok: true, value }` 或 `{ ok: false, error }`。存储、损坏或缺少 durability listener 等操作故障会产生 reject,不会被误标为业务错误。
40
+ `TypertRemoteService` 与 `@Remote` 将 `MessageFeedbackService` 的同三个方法发布出去;Host endpoint 名称为 `messageFeedback.list`、`messageFeedback.put` 与 `messageFeedback.delete`。每个方法都返回判别式业务 union:`{ ok: true, value }` 或 `{ ok: false, error }`。存储、损坏或缺少 durability listener 等操作故障会产生 reject,不会被误标为业务错误。
41
41
 
42
42
  | 方法 | 请求 | 成功 `value` | 拒绝的 `error.code` |
43
43
  |---|---|---|---|
@@ -77,7 +77,7 @@ Plugin disposal 会先关闭变更接纳,排空已进入各个 Session 队列
77
77
 
78
78
  - **缺少客户端聚合与 UI**——Host Remote 契约已经发布,但客户端 Remote 聚合 contribution 与任何 UI 消费方由各自边界负责并保持延后。
79
79
  - **Compare-and-set 仅限单进程**——按 Session 划分的队列只串行化一个服务实例;storage-domain 不提供跨进程条件写,因此多个 Host 进程写入同一存储根目录时仍可能丢失更新。
80
- - **没有持久 Session 删除级联**——Session persistence 没有删除接口,且 `session/disposed`/`host/session-removed` 表示 detach 而非持久删除。因此服务会保留空行,并可能在带外移除日志后留下孤儿行,而不会在 detach 时删除仍有效的反馈。
80
+ - **没有持久 Session 删除级联**——Session persistence 没有删除接口,且 `session/disposed`/`host/session-removed` 表示 detach 而非持久删除。因此服务会保留空行,并可能在带外移除日志后留下遗留行,而不会在 detach 时删除仍有效的反馈。
81
81
  - **Detach/catalog retirement 窗口**——请求若恰好落在 live detach 之后、persistence catalog 物化 header 之前的极短窗口,可能收到 `session-not-found`;调用方应在 retirement materialization 后重试。
82
82
  - **Header 身份不是内容指纹**——只有 `{createdAt, cwd}` 不同时才能识别复用;本契约无法区分保留相同 header 身份的克隆日志。
83
83
  - **调用方边界受信任**——`list`/`put`/`delete` 不携带已认证的 actor 或审计身份。在加入授权与归属信息前,部署方必须只通过受信任或另行认证的边界暴露 Host gateway。
package/lib/index.js CHANGED
@@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto";
3
3
  import { Service } from "@deepseek-ai/cordis";
4
4
  import s from "@deepseek-ai/schemastery";
5
5
  import { deriveEventMessage, isAppendSurfaceEvent } from "@deepseek-ai/dsh-session/surface";
6
- import { GatewayService, Remote } from "@deepseek-ai/dsh-type-meta";
6
+ import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
7
7
  import { z } from "zod";
8
8
  import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
9
9
  //#region lib/types/spec.js
@@ -185,7 +185,7 @@ function nextVersion() {
185
185
  * never creates or resumes an Agent or Session.
186
186
  */
187
187
  let MessageFeedbackService = (() => {
188
- let _classSuper = GatewayService;
188
+ let _classSuper = TypertRemoteService;
189
189
  let _instanceExtraInitializers = [];
190
190
  let _list_decorators;
191
191
  let _put_decorators;
@@ -1,25 +1,26 @@
1
1
  /* Generated by @deepseek-ai/dsh-typert-generator from the Host FaceModel — do not edit. */
2
2
  import type {
3
- TypeRTRemoteContribution,
4
- } from '@deepseek-ai/dsh-type-meta'
3
+ RemoteResult,
4
+ TypertRemoteContribution,
5
+ } from '@deepseek-ai/dsh-typert-protocol'
5
6
  import type { MessageFeedbackDeleteRequest, MessageFeedbackDeleteResult, MessageFeedbackListRequest, MessageFeedbackListResult, MessageFeedbackPutRequest, MessageFeedbackPutResult } from '@deepseek-ai/dsh-message-feedback/types'
6
7
 
7
- declare module '@deepseek-ai/dsh-type-meta' {
8
- interface TypeRTRemoteNamespace$6d657373616765466565646261636b {
9
- delete: (request: MessageFeedbackDeleteRequest) => Promise<MessageFeedbackDeleteResult>
10
- list: (request: MessageFeedbackListRequest) => Promise<MessageFeedbackListResult>
11
- put: (request: MessageFeedbackPutRequest) => Promise<MessageFeedbackPutResult>
8
+ declare module '@deepseek-ai/dsh-typert-protocol' {
9
+ interface TypertRemoteNamespace$6d657373616765466565646261636b {
10
+ delete: (request: MessageFeedbackDeleteRequest) => Promise<RemoteResult<MessageFeedbackDeleteResult>>
11
+ list: (request: MessageFeedbackListRequest) => Promise<RemoteResult<MessageFeedbackListResult>>
12
+ put: (request: MessageFeedbackPutRequest) => Promise<RemoteResult<MessageFeedbackPutResult>>
12
13
  }
13
- interface TypeRTRemoteMap {
14
- 'messageFeedback/delete': (request: MessageFeedbackDeleteRequest) => Promise<MessageFeedbackDeleteResult>
15
- 'messageFeedback/list': (request: MessageFeedbackListRequest) => Promise<MessageFeedbackListResult>
16
- 'messageFeedback/put': (request: MessageFeedbackPutRequest) => Promise<MessageFeedbackPutResult>
14
+ interface TypertRemoteMap {
15
+ 'messageFeedback/delete': (request: MessageFeedbackDeleteRequest) => Promise<RemoteResult<MessageFeedbackDeleteResult>>
16
+ 'messageFeedback/list': (request: MessageFeedbackListRequest) => Promise<RemoteResult<MessageFeedbackListResult>>
17
+ 'messageFeedback/put': (request: MessageFeedbackPutRequest) => Promise<RemoteResult<MessageFeedbackPutResult>>
17
18
  }
18
- interface TypeRTRemoteNamespaceMap {
19
- 'messageFeedback': TypeRTRemoteNamespace$6d657373616765466565646261636b
19
+ interface TypertRemoteNamespaceMap {
20
+ 'messageFeedback': TypertRemoteNamespace$6d657373616765466565646261636b
20
21
  }
21
22
  }
22
23
 
23
- export declare const TYPERT_REMOTE: TypeRTRemoteContribution
24
+ export declare const TYPERT_REMOTE: TypertRemoteContribution
24
25
  export default TYPERT_REMOTE
25
26
  //# sourceMappingURL=typert.remote-client.d.ts.map
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { Context, Service } from '@deepseek-ai/cordis';
6
6
  import s from '@deepseek-ai/schemastery';
7
- import { GatewayService } from '@deepseek-ai/dsh-type-meta';
7
+ import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
8
8
  import type { MessageFeedbackDeleteRequest, MessageFeedbackDeleteResult, MessageFeedbackListRequest, MessageFeedbackListResult, MessageFeedbackPutRequest, MessageFeedbackPutResult } from './types.ts';
9
9
  export type * from './types.ts';
10
10
  export { messageFeedbackDomainSpec, messageFeedbackItemSchema, messageFeedbackRatingSchema, messageFeedbackRowSchema, messageFeedbackSessionIdentitySchema, messageFeedbackVersionSchema, } from './spec.ts';
@@ -23,7 +23,7 @@ declare module '@deepseek-ai/cordis' {
23
23
  * Storage-domain sidecar service. It inspects persisted Session history and
24
24
  * never creates or resumes an Agent or Session.
25
25
  */
26
- export declare class MessageFeedbackService extends GatewayService {
26
+ export declare class MessageFeedbackService extends TypertRemoteService {
27
27
  static inject: string[];
28
28
  /** Loader validation for the required note-size policy. */
29
29
  static Config: s<Config>;
@@ -41,7 +41,7 @@ import { randomUUID } from 'node:crypto';
41
41
  import { Service } from '@deepseek-ai/cordis';
42
42
  import s from '@deepseek-ai/schemastery';
43
43
  import { deriveEventMessage, isAppendSurfaceEvent } from '@deepseek-ai/dsh-session/surface';
44
- import { GatewayService, Remote } from '@deepseek-ai/dsh-type-meta';
44
+ import { TypertRemoteService, Remote } from '@deepseek-ai/dsh-typert-protocol';
45
45
  import { messageFeedbackDomainSpec } from "./spec.js";
46
46
  export { messageFeedbackDomainSpec, messageFeedbackItemSchema, messageFeedbackRatingSchema, messageFeedbackRowSchema, messageFeedbackSessionIdentitySchema, messageFeedbackVersionSchema, } from "./spec.js";
47
47
  /** Immutable empty list reused only as an input to caller-owned copying. */
@@ -109,7 +109,7 @@ function nextVersion() {
109
109
  * never creates or resumes an Agent or Session.
110
110
  */
111
111
  let MessageFeedbackService = (() => {
112
- let _classSuper = GatewayService;
112
+ let _classSuper = TypertRemoteService;
113
113
  let _instanceExtraInitializers = [];
114
114
  let _list_decorators;
115
115
  let _put_decorators;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-message-feedback",
3
3
  "description": "Lifecycle-bound per-message rating and note sidecar for the DeepSeek Harness",
4
- "version": "0.0.1-rc.2",
4
+ "version": "0.0.1-rc.3",
5
5
  "publishConfig": {
6
6
  "access": "restricted"
7
7
  },
@@ -45,38 +45,36 @@
45
45
  "lib/typert.host.js",
46
46
  "lib/typert.host.d.ts",
47
47
  "lib/typert.remote-client.js",
48
- "lib/typert.remote-client.d.ts",
49
- "lib/typert.remote-client.d.ts.map",
50
- "src"
48
+ "lib/typert.remote-client.d.ts"
51
49
  ],
52
50
  "license": "BSD-3-Clause",
53
51
  "peerDependencies": {
54
- "@deepseek-ai/dsh-brand": "^0.0.1-rc.2",
55
- "@deepseek-ai/dsh-invariants": "^0.0.1-rc.2",
56
- "@deepseek-ai/dsh-session": "^0.0.1-rc.2",
57
- "@deepseek-ai/dsh-llm": "^0.0.1-rc.2",
58
- "@deepseek-ai/dsh-session-persistence": "^0.0.1-rc.2",
59
- "@deepseek-ai/dsh-type-meta": "^0.0.1-rc.2",
60
- "@deepseek-ai/cordis": "^4.0.1-rc.1",
61
- "@deepseek-ai/dsh-storage-domain": "^0.0.1-rc.2"
52
+ "@deepseek-ai/dsh-brand": "^0.0.1-rc.3",
53
+ "@deepseek-ai/dsh-invariants": "^0.0.1-rc.3",
54
+ "@deepseek-ai/dsh-llm": "^0.0.1-rc.3",
55
+ "@deepseek-ai/dsh-session": "^0.0.1-rc.3",
56
+ "@deepseek-ai/dsh-session-persistence": "^0.0.1-rc.3",
57
+ "@deepseek-ai/dsh-storage-domain": "^0.0.1-rc.3",
58
+ "@deepseek-ai/dsh-typert-protocol": "^0.0.1-rc.3",
59
+ "@deepseek-ai/cordis": "^4.0.1-rc.1"
62
60
  },
63
61
  "dependencies": {
64
62
  "zod": "^4.4.3",
65
63
  "@deepseek-ai/schemastery": "^3.18.1-rc.1"
66
64
  },
67
65
  "devDependencies": {
68
- "@deepseek-ai/cordis-plugin-include": "^1.0.5-rc.1",
69
66
  "@deepseek-ai/cordis-plugin-loader": "^1.0.1-rc.1",
70
- "@deepseek-ai/dsh-brand": "^0.0.1-rc.2",
71
- "@deepseek-ai/dsh-invariants": "^0.0.1-rc.2",
72
- "@deepseek-ai/dsh-llm": "^0.0.1-rc.2",
73
- "@deepseek-ai/dsh-session-persistence": "^0.0.1-rc.2",
74
- "@deepseek-ai/dsh-session": "^0.0.1-rc.2",
75
- "@deepseek-ai/dsh-session-persistence-jsonl": "^0.0.1-rc.2",
76
- "@deepseek-ai/dsh-storage": "^0.0.1-rc.2",
77
- "@deepseek-ai/dsh-storage-domain": "^0.0.1-rc.2",
78
- "@deepseek-ai/dsh-storage-json": "^0.0.1-rc.2",
79
- "@deepseek-ai/dsh-type-meta": "^0.0.1-rc.2",
67
+ "@deepseek-ai/dsh-brand": "^0.0.1-rc.3",
68
+ "@deepseek-ai/dsh-invariants": "^0.0.1-rc.3",
69
+ "@deepseek-ai/dsh-llm": "^0.0.1-rc.3",
70
+ "@deepseek-ai/cordis-plugin-include": "^1.0.5-rc.1",
71
+ "@deepseek-ai/dsh-session": "^0.0.1-rc.3",
72
+ "@deepseek-ai/dsh-session-persistence": "^0.0.1-rc.3",
73
+ "@deepseek-ai/dsh-session-persistence-jsonl": "^0.0.1-rc.3",
74
+ "@deepseek-ai/dsh-storage": "^0.0.1-rc.3",
75
+ "@deepseek-ai/dsh-storage-domain": "^0.0.1-rc.3",
76
+ "@deepseek-ai/dsh-storage-json": "^0.0.1-rc.3",
77
+ "@deepseek-ai/dsh-typert-protocol": "^0.0.1-rc.3",
80
78
  "@deepseek-ai/cordis": "^4.0.1-rc.1"
81
79
  }
82
80
  }
@@ -1 +0,0 @@
1
- {"version":3,"file":"typert.remote-client.d.ts","names":["delete","list","put"],"sources":["../src/index.ts"],"sourcesContent":[null],"mappings":";;;;;;;;IA+QEA,M;IAlFMC,I;IAgBNC,G;;;IAkEAF,wB;IAlFMC,sB;IAgBNC,qB","ignoreList":[]}
package/src/index.ts DELETED
@@ -1,383 +0,0 @@
1
- /**
2
- * Durable, lifecycle-bound feedback for finalized assistant messages.
3
- * @module @deepseek-ai/dsh-message-feedback
4
- */
5
-
6
- import { Buffer } from 'node:buffer'
7
- import { randomUUID } from 'node:crypto'
8
- import { Context, Service } from '@deepseek-ai/cordis'
9
- import s from '@deepseek-ai/schemastery'
10
- import { deriveEventMessage, isAppendSurfaceEvent } from '@deepseek-ai/dsh-session/surface'
11
- import type { SessionHeader, SessionId } from '@deepseek-ai/dsh-session/types'
12
- import type { SessionInspection } from '@deepseek-ai/dsh-session-persistence'
13
- import type { KvTable } from '@deepseek-ai/dsh-storage-domain'
14
- import { GatewayService, Remote } from '@deepseek-ai/dsh-type-meta'
15
- import { messageFeedbackDomainSpec } from './spec.ts'
16
- import type { MessageFeedbackRow, MessageFeedbackSessionIdentity } from './spec.ts'
17
- import type {
18
- MessageFeedbackDeleteRequest,
19
- MessageFeedbackDeleteResult,
20
- MessageFeedbackDeleteValue,
21
- MessageFeedbackFailure,
22
- MessageFeedbackItem,
23
- MessageFeedbackListRequest,
24
- MessageFeedbackListResult,
25
- MessageFeedbackListValue,
26
- MessageFeedbackNoteBlank,
27
- MessageFeedbackNoteTooLarge,
28
- MessageFeedbackPutRequest,
29
- MessageFeedbackPutResult,
30
- MessageFeedbackRejected,
31
- MessageFeedbackSessionNotFound,
32
- MessageFeedbackSuccess,
33
- MessageFeedbackVersion,
34
- MessageFeedbackVersionConflict,
35
- } from './types.ts'
36
-
37
- export type * from './types.ts'
38
- export {
39
- messageFeedbackDomainSpec,
40
- messageFeedbackItemSchema,
41
- messageFeedbackRatingSchema,
42
- messageFeedbackRowSchema,
43
- messageFeedbackSessionIdentitySchema,
44
- messageFeedbackVersionSchema,
45
- } from './spec.ts'
46
- export type { MessageFeedbackRow, MessageFeedbackSessionIdentity } from './spec.ts'
47
-
48
- /** Required deployment policy for optional notes. */
49
- export interface Config {
50
- /** Maximum UTF-8 byte length accepted for one note. */
51
- readonly maxNoteBytes: number
52
- }
53
-
54
- declare module '@deepseek-ai/cordis' {
55
- interface Context {
56
- messageFeedback: MessageFeedbackService
57
- }
58
- }
59
-
60
- /** Immutable empty list reused only as an input to caller-owned copying. */
61
- const EMPTY_ITEMS: readonly MessageFeedbackItem[] = Object.freeze([])
62
-
63
- /** Validate the one deployment-varying limit at the configuration boundary. */
64
- function resolveMaxNoteBytes(value: number): number {
65
- if (!Number.isSafeInteger(value) || value < 1) {
66
- throw new TypeError(
67
- `message-feedback: maxNoteBytes must be a positive safe integer, got ${String(value)}`,
68
- )
69
- }
70
- return value
71
- }
72
-
73
- /** Copy and freeze one item before it crosses the service boundary. */
74
- function snapshotItem(item: MessageFeedbackItem): MessageFeedbackItem {
75
- return Object.freeze({
76
- messageId: item.messageId,
77
- rating: item.rating,
78
- ...(item.note === undefined ? {} : { note: item.note }),
79
- version: item.version,
80
- createdAt: item.createdAt,
81
- updatedAt: item.updatedAt,
82
- })
83
- }
84
-
85
- /** Copy and freeze a list response. */
86
- function snapshotList(items: readonly MessageFeedbackItem[]): MessageFeedbackListValue {
87
- return Object.freeze({ items: Object.freeze(items.map(snapshotItem)) })
88
- }
89
-
90
- /** Build a frozen success branch. */
91
- function success<T>(value: T): MessageFeedbackSuccess<T> {
92
- return Object.freeze({ ok: true, value })
93
- }
94
-
95
- /** Build a frozen business-failure branch. */
96
- function rejected<E extends MessageFeedbackFailure>(error: E): MessageFeedbackRejected<E> {
97
- return Object.freeze({ ok: false, error: Object.freeze(error) })
98
- }
99
-
100
- /** Project the Session fields that distinguish one persisted log lifecycle. */
101
- function identityOf(header: SessionHeader): MessageFeedbackSessionIdentity {
102
- return Object.freeze({
103
- createdAt: header.createdAt,
104
- ...(header.cwd === undefined ? {} : { cwd: header.cwd }),
105
- })
106
- }
107
-
108
- /** Whether a stored row belongs to the inspected Session lifecycle. */
109
- function sameIdentity(row: MessageFeedbackRow, header: SessionHeader): boolean {
110
- return row.session.createdAt === header.createdAt && row.session.cwd === header.cwd
111
- }
112
-
113
- /** Whether two observations name the same persisted Session lifecycle. */
114
- function sameHeaderIdentity(left: SessionHeader, right: SessionHeader): boolean {
115
- return left.id === right.id && left.createdAt === right.createdAt && left.cwd === right.cwd
116
- }
117
-
118
- /** Freeze the replacement row so storage-domain never exposes mutable aliases. */
119
- function rowSnapshot(
120
- session: MessageFeedbackSessionIdentity,
121
- items: readonly MessageFeedbackItem[],
122
- ): MessageFeedbackRow {
123
- const copiedItems = items.map(snapshotItem)
124
- Object.freeze(copiedItems)
125
- return Object.freeze({
126
- session,
127
- items: copiedItems,
128
- })
129
- }
130
-
131
- /** Generate an opaque equality token for one material mutation. */
132
- function nextVersion(): MessageFeedbackVersion {
133
- return randomUUID() as MessageFeedbackVersion
134
- }
135
-
136
- /** Session inspection result that keeps absence inside the business union. */
137
- type KnownSession =
138
- | MessageFeedbackSuccess<SessionInspection>
139
- | MessageFeedbackRejected<MessageFeedbackSessionNotFound>
140
-
141
- /** Validated note or one explicit request failure. */
142
- type ResolvedNote =
143
- | MessageFeedbackSuccess<string | undefined>
144
- | MessageFeedbackRejected<MessageFeedbackNoteBlank | MessageFeedbackNoteTooLarge>
145
-
146
- /**
147
- * Storage-domain sidecar service. It inspects persisted Session history and
148
- * never creates or resumes an Agent or Session.
149
- */
150
- export class MessageFeedbackService extends GatewayService {
151
- static inject = ['storageDomain', 'sessionPersistence', 'sessions']
152
-
153
- /** Loader validation for the required note-size policy. */
154
- static Config: s<Config> = s.object({
155
- maxNoteBytes: s.number().step(1).min(1).required(),
156
- })
157
-
158
- private readonly maxNoteBytes: number
159
- private table?: KvTable<SessionId, MessageFeedbackRow>
160
- private readonly operationTails = new Map<SessionId, Promise<void>>()
161
- private mutationAdmissionOpen = true
162
-
163
- /**
164
- * @param ctx - Host context carrying persistence and the storage-domain form.
165
- * @param config - Required note-size policy.
166
- */
167
- constructor(ctx: Context, config: Config) {
168
- super(ctx, 'messageFeedback')
169
- this.maxNoteBytes = resolveMaxNoteBytes(config.maxNoteBytes)
170
- }
171
-
172
- /** Open and own the one message-feedback sidecar domain. */
173
- protected async [Service.init](): Promise<void> {
174
- const domain = await this.ctx.storageDomain.open(messageFeedbackDomainSpec)
175
- this.ctx.effect(() => async () => {
176
- this.mutationAdmissionOpen = false
177
- await Promise.all(this.operationTails.values())
178
- await domain.close()
179
- }, 'message-feedback.domainClose')
180
- this.table = domain.table('sessions')
181
- }
182
-
183
- /**
184
- * Read feedback belonging to the current persisted Session lifecycle.
185
- * A stale row from a reused Session id is invisible.
186
- * @param request - Session identity to inspect and list.
187
- * @returns current immutable items or `session-not-found`.
188
- */
189
- @Remote('list')
190
- async list(request: MessageFeedbackListRequest): Promise<MessageFeedbackListResult> {
191
- const known = await this.inspectSession(request.sessionId)
192
- if (!known.ok) return known
193
- const row = this.requireTable().get(request.sessionId)
194
- const items = row !== undefined && sameIdentity(row, known.value.meta) ? row.items : EMPTY_ITEMS
195
- return success(snapshotList(items))
196
- }
197
-
198
- /**
199
- * Create or replace feedback for one derived append-origin assistant
200
- * message. Every request must match the addressed item's current version;
201
- * a matching no-op returns the stored item without changing its revision.
202
- * @param request - target, desired value, and observed item version.
203
- * @returns the committed item or an explicit business failure.
204
- */
205
- @Remote('put')
206
- put(request: MessageFeedbackPutRequest): Promise<MessageFeedbackPutResult> {
207
- const note = this.resolveNote(request.note)
208
- if (!note.ok) return Promise.resolve(note)
209
- return this.enqueue(request.sessionId, async () => {
210
- const known = await this.inspectSession(request.sessionId)
211
- if (!known.ok) return known
212
- if (!this.hasFeedbackTarget(known.value, request.messageId)) {
213
- return rejected({
214
- code: 'target-not-found',
215
- sessionId: request.sessionId,
216
- messageId: request.messageId,
217
- })
218
- }
219
-
220
- const durable = await this.ensureTargetDurable(known.value)
221
- if (!sameHeaderIdentity(durable.meta, known.value.meta)
222
- || !this.hasFeedbackTarget(durable, request.messageId)) {
223
- return rejected({
224
- code: 'target-not-found',
225
- sessionId: request.sessionId,
226
- messageId: request.messageId,
227
- })
228
- }
229
-
230
- const table = this.requireTable()
231
- const stored = table.get(request.sessionId)
232
- const current = stored !== undefined && sameIdentity(stored, durable.meta) ? stored : undefined
233
- const items = current?.items ?? EMPTY_ITEMS
234
- const index = items.findIndex(item => item.messageId === request.messageId)
235
- const existing = items[index]
236
- if (request.ifVersion !== (existing?.version ?? null)) {
237
- return rejected(this.versionConflict(existing ?? null))
238
- }
239
- if (existing !== undefined
240
- && existing.rating === request.rating
241
- && existing.note === note.value) {
242
- return success(snapshotItem(existing))
243
- }
244
-
245
- const now = Date.now()
246
- const item = snapshotItem({
247
- messageId: request.messageId,
248
- rating: request.rating,
249
- ...(note.value === undefined ? {} : { note: note.value }),
250
- version: nextVersion(),
251
- createdAt: existing?.createdAt ?? now,
252
- updatedAt: existing === undefined ? now : Math.max(now, existing.updatedAt),
253
- })
254
- const nextItems = [...items]
255
- if (index === -1) nextItems.push(item)
256
- else nextItems[index] = item
257
- await table.put(
258
- request.sessionId,
259
- rowSnapshot(identityOf(durable.meta), nextItems),
260
- )
261
- return success(snapshotItem(item))
262
- })
263
- }
264
-
265
- /**
266
- * Delete one feedback item. Absence is successful regardless of the
267
- * supplied version; an existing item requires an exact version match.
268
- * @param request - Session, message, and observed item version.
269
- * @returns the stable absent postcondition, or an explicit failure.
270
- */
271
- @Remote('delete')
272
- delete(request: MessageFeedbackDeleteRequest): Promise<MessageFeedbackDeleteResult> {
273
- return this.enqueue(request.sessionId, async () => {
274
- const known = await this.inspectSession(request.sessionId)
275
- if (!known.ok) return known
276
-
277
- const table = this.requireTable()
278
- const stored = table.get(request.sessionId)
279
- const current = stored !== undefined && sameIdentity(stored, known.value.meta) ? stored : undefined
280
- const items = current?.items ?? EMPTY_ITEMS
281
- const existing = items.find(item => item.messageId === request.messageId)
282
- if (existing === undefined) {
283
- return success<MessageFeedbackDeleteValue>(Object.freeze({ absent: true }))
284
- }
285
- if (request.ifVersion !== existing.version) {
286
- return rejected(this.versionConflict(existing))
287
- }
288
-
289
- await table.put(
290
- request.sessionId,
291
- rowSnapshot(identityOf(known.value.meta), items.filter(item => item !== existing)),
292
- )
293
- return success<MessageFeedbackDeleteValue>(Object.freeze({ absent: true }))
294
- })
295
- }
296
-
297
- /**
298
- * Resolve a live owner directly; otherwise use the storage catalog as the
299
- * existence authority before inspecting the log. Inspection failures for a
300
- * catalogued Session remain infrastructure failures rather than being
301
- * guessed into the business `session-not-found` branch.
302
- */
303
- private async inspectSession(sessionId: SessionId): Promise<KnownSession> {
304
- if (this.ctx.sessions.get(sessionId) === undefined) {
305
- const snapshots = await this.ctx.sessionPersistence.listSnapshots()
306
- if (!snapshots.some(snapshot => snapshot.header.id === sessionId)
307
- && this.ctx.sessions.get(sessionId) === undefined) {
308
- return rejected({ code: 'session-not-found', sessionId })
309
- }
310
- }
311
- return success(await this.ctx.sessionPersistence.inspect(sessionId))
312
- }
313
-
314
- /** Require the exact finalized append-origin assistant message projection. */
315
- private hasFeedbackTarget(inspection: SessionInspection, messageId: MessageFeedbackItem['messageId']): boolean {
316
- return inspection.events.some((event) => {
317
- if (event.type !== 'assistant/message' || !isAppendSurfaceEvent(event)) return false
318
- const message = deriveEventMessage(event)
319
- return message?.role === 'assistant' && message.id === messageId
320
- })
321
- }
322
-
323
- /**
324
- * Put the target log prefix behind a durability barrier before its sidecar.
325
- * A live owner flushes through the SessionStore's canonical checkpoint; a
326
- * cold owner is re-read from the physical durable prefix.
327
- */
328
- private async ensureTargetDurable(inspection: SessionInspection): Promise<SessionInspection> {
329
- const live = this.ctx.sessions.get(inspection.meta.id)
330
- if (live !== undefined && sameHeaderIdentity(live.header, inspection.meta)) {
331
- if (!(await this.ctx.sessions.flush(live))) {
332
- throw new Error(
333
- `message-feedback: no durability listener participated for live session '${inspection.meta.id}'`,
334
- )
335
- }
336
- return await this.ctx.sessionPersistence.readFrom(inspection.meta.id, 0)
337
- }
338
- return await this.ctx.sessionPersistence.readFrom(inspection.meta.id, 0)
339
- }
340
-
341
- /** Validate optional-note semantics and the configured complete UTF-8 byte bound. */
342
- private resolveNote(note: string | undefined): ResolvedNote {
343
- if (note === undefined) return success(undefined)
344
- if (note.trim().length === 0) return rejected({ code: 'note-blank' })
345
- const actualBytes = Buffer.byteLength(note, 'utf8')
346
- if (actualBytes > this.maxNoteBytes) {
347
- return rejected({ code: 'note-too-large', maxBytes: this.maxNoteBytes, actualBytes })
348
- }
349
- return success(note)
350
- }
351
-
352
- /** Return the authoritative item needed to reconcile one failed comparison. */
353
- private versionConflict(current: MessageFeedbackItem | null): MessageFeedbackVersionConflict {
354
- return {
355
- code: 'version-conflict',
356
- current: current === null ? null : snapshotItem(current),
357
- }
358
- }
359
-
360
- /** Queue a complete read/compare/write mutation behind this Session's prior mutation. */
361
- private enqueue<T>(sessionId: SessionId, operation: () => Promise<T>): Promise<T> {
362
- if (!this.mutationAdmissionOpen) {
363
- return Promise.reject(new Error('message-feedback: service is disposing'))
364
- }
365
- const previous = this.operationTails.get(sessionId) ?? Promise.resolve()
366
- const result = previous.then(operation)
367
- const tail = result.then(() => undefined, () => undefined)
368
- this.operationTails.set(sessionId, tail)
369
- return result.finally(() => {
370
- if (this.operationTails.get(sessionId) === tail) this.operationTails.delete(sessionId)
371
- })
372
- }
373
-
374
- /** Resolve the initialized durable table or fail a broken service lifecycle. */
375
- private requireTable(): KvTable<SessionId, MessageFeedbackRow> {
376
- if (this.table === undefined) {
377
- throw new Error('message-feedback: durable domain is not initialized')
378
- }
379
- return this.table
380
- }
381
- }
382
-
383
- export default MessageFeedbackService
package/src/invariant.ts DELETED
@@ -1,27 +0,0 @@
1
- /** Package-owned invariant companion. @module @deepseek-ai/dsh-message-feedback/invariant */
2
-
3
- /* jscpd:ignore-start */
4
- import type { Context } from '@deepseek-ai/cordis'
5
- import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
6
-
7
- const PACKAGE_NAME = '@deepseek-ai/dsh-message-feedback'
8
-
9
- /** Cordis companion plugin name. */
10
- export const name = 'message-feedback-invariant'
11
- /** Services required before the companion can reserve and check package ownership. */
12
- export const inject = ['invariants']
13
-
14
- /**
15
- * No runtime invariant: the private typed writer owns current row mutations,
16
- * the domain schema validates rows on reopen, and no second authority exists.
17
- */
18
- const install: InvariantInstaller = Object.assign(() => {}, { inject: ['messageFeedback'] })
19
-
20
- /**
21
- * Register this package's invariant companion.
22
- * @param ctx - Cordis context carrying the invariant service.
23
- * @returns the installed registration's disposer after setup succeeds.
24
- */
25
- export const apply = (ctx: Context): Promise<() => void> =>
26
- Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
27
- /* jscpd:ignore-end */
package/src/spec.ts DELETED
@@ -1,90 +0,0 @@
1
- /**
2
- * Durable storage-domain declaration for lifecycle-bound message feedback.
3
- * @module @deepseek-ai/dsh-message-feedback/src/spec
4
- */
5
-
6
- import { z } from 'zod'
7
- import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
8
- import type { SessionId } from '@deepseek-ai/dsh-session/types'
9
- import { defineDomain, domainTable } from '@deepseek-ai/dsh-storage-domain'
10
- import type { MessageFeedbackItem, MessageFeedbackRating, MessageFeedbackVersion } from './types.ts'
11
-
12
- const nonNegativeSafeInteger = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER)
13
-
14
- /** Runtime schema for the closed rating vocabulary. */
15
- export const messageFeedbackRatingSchema = z.union([
16
- z.literal('positive'),
17
- z.literal('negative'),
18
- ]) satisfies z.ZodType<MessageFeedbackRating>
19
-
20
- /** Runtime schema for one opaque item version stored on disk. */
21
- export const messageFeedbackVersionSchema = z.uuid()
22
- .transform(value => value as MessageFeedbackVersion)
23
-
24
- /** Runtime schema for one current feedback item. */
25
- // Zod infers transformed branded fields structurally, so it cannot name the
26
- // public interface even though every branded output is created below.
27
- export const messageFeedbackItemSchema = z.object({
28
- messageId: z.string().min(1).transform(value => value as MessageId),
29
- rating: messageFeedbackRatingSchema,
30
- note: z.string().refine(note => note.trim().length > 0, {
31
- message: 'message feedback note must contain a non-whitespace character',
32
- }).optional(),
33
- version: messageFeedbackVersionSchema,
34
- createdAt: nonNegativeSafeInteger,
35
- updatedAt: nonNegativeSafeInteger,
36
- }).refine(item => item.updatedAt >= item.createdAt, {
37
- path: ['updatedAt'],
38
- message: 'message feedback updatedAt must not precede createdAt',
39
- }) as unknown as z.ZodType<MessageFeedbackItem>
40
-
41
- /** Persisted Session fields that fence a sidecar row to one log lifecycle. */
42
- export const messageFeedbackSessionIdentitySchema = z.object({
43
- createdAt: nonNegativeSafeInteger,
44
- cwd: z.string().optional(),
45
- })
46
-
47
- /** Persisted lifecycle identity inferred from its durable schema. */
48
- export type MessageFeedbackSessionIdentity = z.infer<typeof messageFeedbackSessionIdentitySchema>
49
-
50
- /**
51
- * One whole-Session sidecar. Duplicate message ids would make item lookup
52
- * ambiguous; duplicate versions would break their independent identity.
53
- */
54
- export const messageFeedbackRowSchema = z.object({
55
- session: messageFeedbackSessionIdentitySchema,
56
- items: z.array(messageFeedbackItemSchema),
57
- }).superRefine((row, ctx) => {
58
- const messageIds = new Set<string>()
59
- const versions = new Set<string>()
60
- row.items.forEach((item, index) => {
61
- if (messageIds.has(item.messageId)) {
62
- ctx.addIssue({
63
- code: 'custom',
64
- path: ['items', index, 'messageId'],
65
- message: `duplicate message feedback id '${item.messageId}'`,
66
- })
67
- }
68
- messageIds.add(item.messageId)
69
- if (versions.has(item.version)) {
70
- ctx.addIssue({
71
- code: 'custom',
72
- path: ['items', index, 'version'],
73
- message: `duplicate message feedback version '${item.version}'`,
74
- })
75
- }
76
- versions.add(item.version)
77
- })
78
- })
79
-
80
- /** Durable sidecar row inferred from {@link messageFeedbackRowSchema}. */
81
- export type MessageFeedbackRow = z.infer<typeof messageFeedbackRowSchema>
82
-
83
- /** One lifecycle-bound sidecar record per Session id. */
84
- export const messageFeedbackDomainSpec = defineDomain({
85
- name: 'message_feedback',
86
- version: 0,
87
- tables: {
88
- sessions: domainTable<SessionId, MessageFeedbackRow>(messageFeedbackRowSchema),
89
- },
90
- })
package/src/types.ts DELETED
@@ -1,147 +0,0 @@
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
-
8
- import type { Branded } from '@deepseek-ai/dsh-brand'
9
- import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
10
- import type { SessionId } from '@deepseek-ai/dsh-session/types'
11
-
12
- /** Opaque compare-and-set token for one exact feedback item revision. */
13
- export type MessageFeedbackVersion = Branded<'MessageFeedbackVersion'>
14
-
15
- /** The human's overall judgment of one assistant message. */
16
- export type MessageFeedbackRating = 'positive' | 'negative'
17
-
18
- /** One current feedback value and its opaque mutation token. */
19
- export interface MessageFeedbackItem {
20
- /** Stable identity of the assistant message inside the owning Session. */
21
- readonly messageId: MessageId
22
- /** Overall positive or negative judgment. */
23
- readonly rating: MessageFeedbackRating
24
- /** Optional explanation, preserved verbatim after validation. */
25
- readonly note?: string
26
- /** Equality-only token replaced by every material create or update. */
27
- readonly version: MessageFeedbackVersion
28
- /** Host-assigned creation time in Unix epoch milliseconds. */
29
- readonly createdAt: number
30
- /** Host-assigned time of the most recent material update. */
31
- readonly updatedAt: number
32
- }
33
-
34
- /** Read all message feedback belonging to one persisted Session lifecycle. */
35
- export interface MessageFeedbackListRequest {
36
- /** Persisted Session whose sidecar should be read. */
37
- readonly sessionId: SessionId
38
- }
39
-
40
- /** Current feedback values for one Session, in first-creation order. */
41
- export interface MessageFeedbackListValue {
42
- /** Fresh immutable item snapshots. */
43
- readonly items: readonly MessageFeedbackItem[]
44
- }
45
-
46
- /** Create or replace feedback for one assistant message. */
47
- export interface MessageFeedbackPutRequest {
48
- /** Persisted Session that owns the target message. */
49
- readonly sessionId: SessionId
50
- /** Target assistant-message identity. */
51
- readonly messageId: MessageId
52
- /** Desired overall judgment. */
53
- readonly rating: MessageFeedbackRating
54
- /** Optional non-blank explanation. */
55
- readonly note?: string
56
- /** Observed item version, or `null` to require that no item exists. */
57
- readonly ifVersion: MessageFeedbackVersion | null
58
- }
59
-
60
- /** Delete feedback for one message after observing its current version. */
61
- export interface MessageFeedbackDeleteRequest {
62
- /** Persisted Session that owns the sidecar. */
63
- readonly sessionId: SessionId
64
- /** Message whose feedback should be absent after this operation. */
65
- readonly messageId: MessageId
66
- /** Observed item version; ignored when the item is already absent. */
67
- readonly ifVersion: MessageFeedbackVersion
68
- }
69
-
70
- /** Idempotent deletion acknowledgement. */
71
- export interface MessageFeedbackDeleteValue {
72
- /** Stable postcondition shared by the first deletion and every retry. */
73
- readonly absent: true
74
- }
75
-
76
- /** No persisted Session header exists for the requested id. */
77
- export interface MessageFeedbackSessionNotFound {
78
- readonly code: 'session-not-found'
79
- readonly sessionId: SessionId
80
- }
81
-
82
- /** The id does not name a derived, append-origin assistant message. */
83
- export interface MessageFeedbackTargetNotFound {
84
- readonly code: 'target-not-found'
85
- readonly sessionId: SessionId
86
- readonly messageId: MessageId
87
- }
88
-
89
- /** A material mutation did not match the addressed item's current version. */
90
- export interface MessageFeedbackVersionConflict {
91
- readonly code: 'version-conflict'
92
- /** Authoritative current item, or `null` when it does not exist. */
93
- readonly current: MessageFeedbackItem | null
94
- }
95
-
96
- /** A supplied note contains no non-whitespace character. */
97
- export interface MessageFeedbackNoteBlank {
98
- readonly code: 'note-blank'
99
- }
100
-
101
- /** A supplied note exceeds the configured UTF-8 byte limit. */
102
- export interface MessageFeedbackNoteTooLarge {
103
- readonly code: 'note-too-large'
104
- readonly maxBytes: number
105
- readonly actualBytes: number
106
- }
107
-
108
- /** Failures shared by the public message-feedback operations. */
109
- export type MessageFeedbackFailure =
110
- | MessageFeedbackSessionNotFound
111
- | MessageFeedbackTargetNotFound
112
- | MessageFeedbackVersionConflict
113
- | MessageFeedbackNoteBlank
114
- | MessageFeedbackNoteTooLarge
115
-
116
- /** Successful public operation result. */
117
- export interface MessageFeedbackSuccess<T> {
118
- readonly ok: true
119
- readonly value: T
120
- }
121
-
122
- /** Rejected public operation result with a stable business failure. */
123
- export interface MessageFeedbackRejected<E extends MessageFeedbackFailure> {
124
- readonly ok: false
125
- readonly error: E
126
- }
127
-
128
- /** Result returned by the message-feedback `list` operation. */
129
- export type MessageFeedbackListResult =
130
- | MessageFeedbackSuccess<MessageFeedbackListValue>
131
- | MessageFeedbackRejected<MessageFeedbackSessionNotFound>
132
-
133
- /** Result returned by the message-feedback `put` operation. */
134
- export type MessageFeedbackPutResult =
135
- | MessageFeedbackSuccess<MessageFeedbackItem>
136
- | MessageFeedbackRejected<
137
- | MessageFeedbackSessionNotFound
138
- | MessageFeedbackTargetNotFound
139
- | MessageFeedbackVersionConflict
140
- | MessageFeedbackNoteBlank
141
- | MessageFeedbackNoteTooLarge
142
- >
143
-
144
- /** Result returned by the message-feedback `delete` operation. */
145
- export type MessageFeedbackDeleteResult =
146
- | MessageFeedbackSuccess<MessageFeedbackDeleteValue>
147
- | MessageFeedbackRejected<MessageFeedbackSessionNotFound | MessageFeedbackVersionConflict>