@deepseek-ai/dsh-session-title 0.1.1-rc.1 → 0.1.2-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 +114 -20
- package/README.zh.md +117 -23
- package/lib/index.js +121 -60
- package/lib/types/index.d.ts +17 -50
- package/lib/types/index.js +132 -67
- package/lib/types/types.d.ts +60 -2
- package/lib/types/types.js +2 -2
- package/package.json +20 -16
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/session/session-title/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: debc737e3f572f15856f7f78a58abe8532ae15dc
|
|
6
|
+
README.zh.md: cf8079cd5566023f3dc1da8480eeec5327eebb5a
|
package/README.md
CHANGED
|
@@ -1,38 +1,117 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Log-backed session titles for users and maintainers choosing a title source, configuring the service, or debugging title state."
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# @deepseek-ai/dsh-session-title
|
|
2
7
|
|
|
3
8
|
English | [中文](README.zh.md)
|
|
4
9
|
|
|
5
|
-
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
`dsh-session-title` gives every session a title clients can display: a deterministic fallback from the first eligible human message, an optional asynchronous provider (such as a model-backed one), or an explicit user rename. Every accepted revision is a log-only `session/title` event, so titles survive replay, resume, and paging exactly like any other session event and never enter the model surface. The service owns scheduling and acceptance; the optional provider owns generation. Automatic work never delays the main agent response, and a newer revision supersedes older work. Configuration and title sources come first; the implementation internals live in a collapsible developer section below.
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
15
|
+
|
|
16
|
+
- [Use this package](#use-this-package)
|
|
17
|
+
- [Understand the implementation](#understand-the-implementation)
|
|
18
|
+
- [Further Exploration](#further-exploration)
|
|
19
|
+
- [Model Experience](#model-experience)
|
|
20
|
+
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
|
21
|
+
- [Dev Note](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## Use this package
|
|
27
|
+
|
|
28
|
+
Mount the service to give sessions titles that clients can display and that never reach the model. The common path is explicit: load the session store, mount the service with its required limits, and optionally mount one provider plugin.
|
|
29
|
+
|
|
30
|
+
### Choosing a title source
|
|
31
|
+
|
|
32
|
+
Titles come from three sources, newest wins. The built-in fallback derives from the first eligible human message's leading words within the configured caps; a registered provider generates a title over eligible messages; an explicit `rename()` accepts a user-supplied title. Only text blocks from human `user/message` events are eligible, and empty or non-text prompts wait for later eligible input. A user-sourced latest title pins the session — later user messages schedule no automatic revision, and an explicit `refresh()` remains the deliberate unpin.
|
|
33
|
+
|
|
34
|
+
### Minimal configuration
|
|
35
|
+
|
|
36
|
+
All limits are required; the library supplies no defaults. Mount the service with the three bounds:
|
|
37
|
+
|
|
38
|
+
```yaml
|
|
39
|
+
- name: '@deepseek-ai/dsh-session'
|
|
40
|
+
- name: '@deepseek-ai/dsh-session-title'
|
|
41
|
+
config:
|
|
42
|
+
fallbackMaxWords: 8
|
|
43
|
+
fallbackMaxBytes: 96
|
|
44
|
+
maxTitleBytes: 120
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
| Field | Default | Meaning |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| `fallbackMaxWords` | required | Maximum whitespace-delimited words in the deterministic fallback |
|
|
50
|
+
| `fallbackMaxBytes` | required | Maximum UTF-8 bytes in the fallback; must not exceed `maxTitleBytes` |
|
|
51
|
+
| `maxTitleBytes` | required | Maximum UTF-8 bytes accepted from any source |
|
|
52
|
+
|
|
53
|
+
The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-session-title) is the exhaustive source for every accepted field and its JSDoc.
|
|
54
|
+
|
|
55
|
+
### Adding a provider
|
|
56
|
+
|
|
57
|
+
One optional asynchronous provider may be registered through `ctx.sessionTitle.register(provider)`; a second registration throws. The shipped model-backed providers are [first-prompt](../session-title-first-prompt-llm/README.md) and [all-prompts](../session-title-all-prompts-llm/README.md), both using the shared [LLM generation policy](../session-title-llm/README.md). A provider starts only after a marked loop-built request's exact route matches the logged `request/header`, and a newer revision supersedes and aborts older work.
|
|
58
|
+
|
|
59
|
+
### Reading titles
|
|
6
60
|
|
|
7
|
-
|
|
61
|
+
`get(session)` reads the latest folded title from one live or replayed session, and `foldSessionTitle(events)` is the pure fold over a log. The service requires `ctx.sessionProjections` and registers two units: the client-visible `title` unit (the accepted title string for client list rows) and the host-only `titleInput` unit, which folds the first and latest eligible messages plus their count so scheduling and fallback reads are O(1) through `stateOf()`; the full eligible prefix for one provider generation is scanned from the session log at execution time. An explicit `refresh(session)` materializes the fallback when needed, then explicitly runs the registered provider over the current eligible messages.
|
|
8
62
|
|
|
9
|
-
|
|
63
|
+
### Failures and recovery
|
|
10
64
|
|
|
11
|
-
|
|
12
|
-
- `refresh(session, signal?)` materializes the fallback when needed, then explicitly runs the registered provider over the current eligible messages. Provider errors and caller cancellation reject; cancellation does not roll back an already accepted fallback event.
|
|
13
|
-
- `rename(session, title)` accepts an explicit user title synchronously: it normalizes the text, supersedes in-flight automatic work, and appends a `session/title` event with the `user` source. A user-sourced latest title pins the session — later user messages schedule no automatic revision; an explicit `refresh` remains the deliberate unpin.
|
|
14
|
-
- `register(provider)` installs the sole optional provider and returns its awaitable Cordis effect disposer. A second registration throws immediately; disposal aborts pending and active calls, waits for their settlement, and only then permits another provider to register.
|
|
65
|
+
Automatic failures warn and retain the latest title; explicit `refresh()` rejects on provider error or caller cancellation, and cancellation does not roll back an already accepted fallback event. Automatic work never delays the main agent response, its late completion appends a standalone log-only event without opening a turn, and a stale completion cannot append. Forks inherit title events in their seed unchanged.
|
|
15
66
|
|
|
16
|
-
|
|
67
|
+
-----
|
|
17
68
|
|
|
18
|
-
|
|
69
|
+
<a id="understand-the-implementation"></a>
|
|
70
|
+
## Understand the implementation
|
|
19
71
|
|
|
20
|
-
|
|
72
|
+
<details>
|
|
73
|
+
<summary>Implementation internals — click to expand</summary>
|
|
21
74
|
|
|
22
|
-
|
|
75
|
+
This section explains the title design; the observable behavior is fully covered in [Use this package](#use-this-package).
|
|
23
76
|
|
|
24
|
-
|
|
77
|
+
### Design concept
|
|
78
|
+
|
|
79
|
+
Titles are durable, log-only state: every accepted revision is a `session/title` event, and `foldSessionTitle()` selects the latest, so a title survives replay, resume, and paging exactly like any other session event. The service owns scheduling, supersession, and acceptance; providers own generation.
|
|
80
|
+
|
|
81
|
+
### Source map
|
|
82
|
+
|
|
83
|
+
| File | Role |
|
|
25
84
|
|---|---|
|
|
26
|
-
| `
|
|
27
|
-
| `
|
|
28
|
-
| `
|
|
85
|
+
| [`src/index.ts`](src/index.ts) | Service: config, fold, fallback scheduling, provider registry, concurrency, `title` projection unit |
|
|
86
|
+
| [`src/normalize.ts`](src/normalize.ts) | Title-text cleaning, UTF-8-safe truncation, and the deterministic fallback |
|
|
87
|
+
| [`src/types.ts`](src/types.ts) | One home of the `title` projection-key declaration |
|
|
88
|
+
|
|
89
|
+
### Lifecycle and concurrency
|
|
90
|
+
|
|
91
|
+
Per-session work state tracks a revision counter, an in-flight fallback, and pending and active provider work. A newer user message, provider disposal, session disposal, or explicit refresh aborts older work through an `AbortController`; a completion whose provider, revision, session, or signal is stale cannot append. Explicit refreshes reserve their revision before provider work; overlapping automatic and explicit fallback requests share one session-local in-flight append. Service teardown cancels queued work and drains calls that ignore cancellation before unloading completes.
|
|
92
|
+
|
|
93
|
+
### Normalization
|
|
94
|
+
|
|
95
|
+
Accepted titles are cleaned of terminal control sequences, directional and invisible controls, and non-whitespace C0/C1 controls; whitespace is normalized, and truncation to the byte caps never splits a Unicode code point. The deterministic fallback takes the first eligible message's leading words within `fallbackMaxWords` and `fallbackMaxBytes`.
|
|
96
|
+
|
|
97
|
+
</details>
|
|
98
|
+
|
|
99
|
+
-----
|
|
29
100
|
|
|
30
|
-
|
|
101
|
+
<a id="further-exploration"></a>
|
|
102
|
+
## Further Exploration
|
|
31
103
|
|
|
32
|
-
|
|
104
|
+
Read these pages when the service contract is not enough. They move from the subsystem reference to the model-backed providers that plug in here.
|
|
33
105
|
|
|
34
|
-
|
|
106
|
+
- [Session title subsystem](../../../docs/subsystems/session-title.md) — durable title state and the provider vocabulary types.
|
|
107
|
+
- [Shared LLM title policy](../session-title-llm/README.md) — the model-backed generation helper both shipped providers use.
|
|
108
|
+
- [First-message title provider](../session-title-first-prompt-llm/README.md) — titles from the first eligible human message.
|
|
109
|
+
- [All-messages title provider](../session-title-all-prompts-llm/README.md) — titles from every eligible human message.
|
|
110
|
+
- [Session package map](../README.md) — adjacent persistence, projection, title, and telemetry packages.
|
|
35
111
|
|
|
112
|
+
-----
|
|
113
|
+
|
|
114
|
+
<a id="model-experience"></a>
|
|
36
115
|
## Model Experience
|
|
37
116
|
|
|
38
117
|
### Session title state
|
|
@@ -51,5 +130,20 @@ None for the main request; title events do not change its reconstructed content
|
|
|
51
130
|
|
|
52
131
|
## Known Limitations and Deferred Work
|
|
53
132
|
|
|
54
|
-
-
|
|
55
|
-
|
|
133
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
These limits define what the title service does not provide. They are current package constraints.
|
|
137
|
+
|
|
138
|
+
- **No title deletion, search, or list indexing** — unpinning back to automatic titles without an explicit `refresh`, search, and list indexing are outside this service.
|
|
139
|
+
- **At most one provider** — the registry deliberately accepts a single implementation, so a deployment cannot compose competing title strategies without writing one provider that owns their precedence.
|
|
140
|
+
|
|
141
|
+
<a id="dev-note"></a>
|
|
142
|
+
### Dev Note
|
|
143
|
+
|
|
144
|
+
<details>
|
|
145
|
+
<summary>Working context for maintainers — click to expand</summary>
|
|
146
|
+
|
|
147
|
+
None.
|
|
148
|
+
|
|
149
|
+
</details>
|
package/README.zh.md
CHANGED
|
@@ -1,43 +1,122 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "面向用户与维护者的日志会话标题说明,用于选择标题来源、配置服务或排查标题状态。"
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# @deepseek-ai/dsh-session-title
|
|
2
7
|
|
|
3
8
|
[English](README.md) | 中文
|
|
4
9
|
|
|
5
|
-
|
|
10
|
+
## 概述
|
|
11
|
+
|
|
12
|
+
`dsh-session-title` 为每个会话提供客户端可以显示的标题:来自第一条符合条件用户消息的确定性回退、一个可选异步提供方(例如模型支持的提供方),或显式用户重命名。每个已接受的修订都是仅写入日志的 `session/title` 事件,因此标题像任何其他会话事件一样在回放、恢复与分页中存活,且绝不进入模型可见面。服务拥有调度与接受;可选提供方负责生成。自动工作绝不会延迟主 agent 响应,较新的修订会取代旧工作。配置与标题来源在前;实现内部细节放在下方可折叠的开发者章节中。
|
|
13
|
+
|
|
14
|
+
## 目录
|
|
15
|
+
|
|
16
|
+
- [使用本包](#use-this-package)
|
|
17
|
+
- [理解实现](#understand-the-implementation)
|
|
18
|
+
- [进一步探索](#further-exploration)
|
|
19
|
+
- [模型体验](#model-experience)
|
|
20
|
+
- [已知限制与延期工作](#known-limitations-and-deferred-work)
|
|
21
|
+
- [开发备注](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## 使用本包
|
|
27
|
+
|
|
28
|
+
挂载服务,让会话获得客户端可以显示、且绝不触及模型的标题。常用路径是显式的:加载会话存储、以必填上限挂载服务,并可选挂载一个提供方插件。
|
|
29
|
+
|
|
30
|
+
### 选择标题来源
|
|
31
|
+
|
|
32
|
+
标题来自三个来源,最新者胜出。内置回退在配置上限内从第一条符合条件用户消息的开头若干词派生;已注册提供方对符合条件的消息生成标题;显式 `rename()` 接受用户提供的标题。只有人类 `user/message` 事件中的文本块符合条件,空提示词或非文本提示词会等待后续符合条件的输入。用户来源的最新标题会钉住会话——后续用户消息不再安排自动修订,显式 `refresh()` 仍是有意的解钉手段。
|
|
33
|
+
|
|
34
|
+
### 最小配置
|
|
35
|
+
|
|
36
|
+
所有上限都是必填项;该库不提供默认值。以三个上限挂载服务:
|
|
37
|
+
|
|
38
|
+
```yaml
|
|
39
|
+
- name: '@deepseek-ai/dsh-session'
|
|
40
|
+
- name: '@deepseek-ai/dsh-session-title'
|
|
41
|
+
config:
|
|
42
|
+
fallbackMaxWords: 8
|
|
43
|
+
fallbackMaxBytes: 96
|
|
44
|
+
maxTitleBytes: 120
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
| 字段 | 默认值 | 含义 |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| `fallbackMaxWords` | 必填 | 确定性回退中以空白分隔的最大词数 |
|
|
50
|
+
| `fallbackMaxBytes` | 必填 | 回退允许的最大 UTF-8 字节数;不得超过 `maxTitleBytes` |
|
|
51
|
+
| `maxTitleBytes` | 必填 | 接受任何来源标题的最大 UTF-8 字节数 |
|
|
52
|
+
|
|
53
|
+
生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-session-title)是每个受支持字段及其 JSDoc 的穷尽式真源。
|
|
54
|
+
|
|
55
|
+
### 添加提供方
|
|
56
|
+
|
|
57
|
+
可选异步提供方可通过 `ctx.sessionTitle.register(provider)` 注册一个;第二次注册会立即抛出。随附的模型支持提供方是[首消息](../session-title-first-prompt-llm/README.zh.md)与[全消息](../session-title-all-prompts-llm/README.zh.md),两者都使用共享的 [LLM 生成策略](../session-title-llm/README.zh.md)。提供方只有在带标记、由循环构建的请求的确切路由与已记录 `request/header` 匹配时才启动,较新的修订会取代并中止旧工作。
|
|
58
|
+
|
|
59
|
+
### 读取标题
|
|
6
60
|
|
|
7
|
-
|
|
61
|
+
`get(session)` 从活跃或回放会话读取折叠出的最新标题,`foldSessionTitle(events)` 是对日志的纯折叠。服务要求 `ctx.sessionProjections` 并注册两个单元:客户端可见的 `title` 单元(供客户端列表行使用的已接受标题字符串)和仅供 host 使用的 `titleInput` 单元——后者折叠第一条与最新一条合格消息及其计数,使调度与回退读取通过 `stateOf()` 达到 O(1);某次提供方生成所需的完整合格前缀,则会在执行时从会话日志中扫描取得。显式 `refresh(session)` 在需要时物化回退,然后对当前符合条件的消息显式运行已注册提供方。
|
|
8
62
|
|
|
9
|
-
|
|
63
|
+
### 失败与恢复
|
|
10
64
|
|
|
11
|
-
|
|
12
|
-
- `refresh(session, signal?)` 在需要时物化回退,然后显式运行已注册提供方,处理当前符合条件的消息。提供方错误或调用方取消都会导致返回的 Promise 被拒绝;取消不会回滚已接受的回退事件。
|
|
13
|
-
- `rename(session, title)` 同步接受用户显式标题:规范化文本、取代在途自动工作,并追加一条 `user` 来源的 `session/title` 事件。用户来源的最新标题会钉住该会话——后续用户消息不再安排自动修订;显式 `refresh` 仍是有意的解钉手段。
|
|
14
|
-
- `register(provider)` 安装唯一可选提供方,并返回可等待的 Cordis effect disposer。第二次注册会立即抛出;对提供方执行 dispose(资源释放)会中止待处理和活跃调用,等待其结算,之后才允许注册另一个提供方。
|
|
65
|
+
自动失败会发出警告并保留最新标题;显式 `refresh()` 在提供方错误或调用方取消时拒绝,取消不会回滚已接受的回退事件。自动工作绝不会延迟主 agent 响应,其延迟完成会追加一个独立纯日志事件而不打开轮次,陈旧的完成结果无法追加。fork 出的会话会原样继承种子中的标题事件。
|
|
15
66
|
|
|
16
|
-
|
|
67
|
+
-----
|
|
17
68
|
|
|
18
|
-
|
|
69
|
+
<a id="understand-the-implementation"></a>
|
|
70
|
+
## 理解实现
|
|
19
71
|
|
|
20
|
-
|
|
72
|
+
<details>
|
|
73
|
+
<summary>实现细节——点击展开</summary>
|
|
21
74
|
|
|
22
|
-
|
|
75
|
+
本节解释标题设计;可观察行为已在[使用本包](#use-this-package)中完整说明。
|
|
23
76
|
|
|
24
|
-
|
|
77
|
+
### 设计理念
|
|
78
|
+
|
|
79
|
+
标题是持久的、仅写入日志的状态:每个已接受的修订都是 `session/title` 事件,`foldSessionTitle()` 选择最新事件,因此标题像任何其他会话事件一样在回放、恢复与分页中存活。服务拥有调度、取代与接受;提供方负责生成。
|
|
80
|
+
|
|
81
|
+
### 源码地图
|
|
82
|
+
|
|
83
|
+
| 文件 | 职责 |
|
|
25
84
|
|---|---|
|
|
26
|
-
| `
|
|
27
|
-
| `
|
|
28
|
-
| `
|
|
85
|
+
| [`src/index.ts`](src/index.ts) | 服务:配置、折叠、回退调度、提供方注册表、并发、`title` 投影单元 |
|
|
86
|
+
| [`src/normalize.ts`](src/normalize.ts) | 标题文本清洗、UTF-8 安全截断与确定性回退 |
|
|
87
|
+
| [`src/types.ts`](src/types.ts) | `title` 投影键声明的唯一归属 |
|
|
88
|
+
|
|
89
|
+
### 生命周期与并发
|
|
90
|
+
|
|
91
|
+
每个会话的工作状态维护一个修订计数器、一个进行中的回退,以及待处理与活跃的提供方工作。较新的用户消息、提供方 dispose(资源释放)、会话 dispose 或显式刷新都会通过 `AbortController` 中止旧工作;提供方、修订、会话或信号已陈旧的完成结果无法追加。显式刷新会在提供方工作之前预留修订号;重叠的自动/显式回退请求共享一个会话本地正在进行的追加操作。服务拆卸会取消排队工作,并在卸载完成前等待不响应取消的调用结算。
|
|
92
|
+
|
|
93
|
+
### 规范化
|
|
94
|
+
|
|
95
|
+
已接受标题会清除终端控制序列、方向性与不可见控制符以及非空白的 C0/C1 控制符;空白被规范化,按字节上限截断时绝不切断 Unicode 码点。确定性回退在 `fallbackMaxWords` 与 `fallbackMaxBytes` 内取第一条符合条件消息的开头若干词。
|
|
96
|
+
|
|
97
|
+
</details>
|
|
98
|
+
|
|
99
|
+
-----
|
|
29
100
|
|
|
30
|
-
|
|
101
|
+
<a id="further-exploration"></a>
|
|
102
|
+
## 进一步探索
|
|
31
103
|
|
|
32
|
-
|
|
104
|
+
当服务约定不够用时阅读以下页面。它们从子系统参考逐步进入在此插拔的模型支持提供方。
|
|
33
105
|
|
|
34
|
-
|
|
106
|
+
- [会话标题子系统](../../../docs/subsystems/session-title.zh.md)——持久标题状态与提供方词汇类型。
|
|
107
|
+
- [共享 LLM 标题策略](../session-title-llm/README.zh.md)——两个随附提供方共用的模型生成辅助模块。
|
|
108
|
+
- [首消息标题提供方](../session-title-first-prompt-llm/README.zh.md)——根据第一条符合条件的用户消息生成标题。
|
|
109
|
+
- [全消息标题提供方](../session-title-all-prompts-llm/README.zh.md)——根据所有符合条件的用户消息生成标题。
|
|
110
|
+
- [会话包映射](../README.zh.md)——相邻的持久化、投影、标题与遥测包。
|
|
35
111
|
|
|
112
|
+
-----
|
|
113
|
+
|
|
114
|
+
<a id="model-experience"></a>
|
|
36
115
|
## 模型体验
|
|
37
116
|
|
|
38
117
|
### 会话标题状态
|
|
39
118
|
|
|
40
|
-
####
|
|
119
|
+
#### 模型看到什么
|
|
41
120
|
|
|
42
121
|
无。`session/title` 只写入日志,绝不会进入会话接口、`deriveMessages()`、系统提示词、工具 schema 或请求前缀。
|
|
43
122
|
|
|
@@ -47,9 +126,24 @@ fork 出的会话会原样继承种子中的标题事件。首消息节奏不会
|
|
|
47
126
|
|
|
48
127
|
#### KV Cache 影响
|
|
49
128
|
|
|
50
|
-
|
|
129
|
+
不影响主请求;标题事件不会改变其重建内容或缓存键。
|
|
130
|
+
|
|
131
|
+
## 已知限制与延期工作
|
|
132
|
+
|
|
133
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
这些限制说明标题服务不提供什么。它们是当前包约束。
|
|
137
|
+
|
|
138
|
+
- **没有标题删除、搜索或列表索引**——不经显式 `refresh` 就解钉回自动标题、搜索与列表索引不属于此服务。
|
|
139
|
+
- **至多一个提供方**——注册表有意只接受一个实现,因此部署若要组合相互竞争的标题策略,必须编写一个自行负责优先级的提供方。
|
|
140
|
+
|
|
141
|
+
<a id="dev-note"></a>
|
|
142
|
+
### 开发备注
|
|
143
|
+
|
|
144
|
+
<details>
|
|
145
|
+
<summary>维护者的工作上下文——点击展开</summary>
|
|
51
146
|
|
|
52
|
-
|
|
147
|
+
无。
|
|
53
148
|
|
|
54
|
-
|
|
55
|
-
- 提供方注册表有意最多接受一个实现,因此部署若要组合相互竞争的标题策略,必须编写一个自行负责优先级的提供方。
|
|
149
|
+
</details>
|
package/lib/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Service } from "@deepseek-ai/cordis";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { z as z$1 } from "zod";
|
|
4
|
-
import {
|
|
4
|
+
import { isAgentLoopRequest } from "@deepseek-ai/dsh-llm";
|
|
5
|
+
import { assertNever, deepFreeze } from "@deepseek-ai/dsh-util-values";
|
|
5
6
|
//#region lib/types/normalize.js
|
|
6
7
|
/** Title text normalization and UTF-8-safe truncation. */
|
|
7
8
|
/** Operating-system-command escape sequences, including unterminated tails. */
|
|
@@ -84,26 +85,100 @@ function SessionTitleProviderId(id) {
|
|
|
84
85
|
var SessionTitleInvalidError = class extends Error {
|
|
85
86
|
name = "SessionTitleInvalidError";
|
|
86
87
|
};
|
|
88
|
+
/** Extract one eligible human text message from a session event. */
|
|
89
|
+
function sessionTitleUserMessageOf(event) {
|
|
90
|
+
if (event.type !== "user/message" || event.data.source.kind !== "user") return void 0;
|
|
91
|
+
const text = event.data.content.filter((block) => block.type === "text").map((block) => block.text).join("\n");
|
|
92
|
+
if (normalizeSessionTitle(text, Number.MAX_SAFE_INTEGER).length === 0) return void 0;
|
|
93
|
+
return {
|
|
94
|
+
seq: event.seq,
|
|
95
|
+
text
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/** Defensive copy of a logged title source (the snapshot must not alias log-owned objects). */
|
|
99
|
+
function copySessionTitleSource(source) {
|
|
100
|
+
switch (source.kind) {
|
|
101
|
+
case "fallback": return { kind: "fallback" };
|
|
102
|
+
case "provider": return {
|
|
103
|
+
kind: "provider",
|
|
104
|
+
provider: source.provider,
|
|
105
|
+
...source.model === void 0 ? {} : { model: { ...source.model } }
|
|
106
|
+
};
|
|
107
|
+
case "user": return { kind: "user" };
|
|
108
|
+
/* v8 ignore next -- closed-union exhaustiveness guard */
|
|
109
|
+
default: return assertNever(source, "SessionTitleSource");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** Validate one positive integer configuration field. */
|
|
113
|
+
function assertPositiveInteger(name, value) {
|
|
114
|
+
if (!Number.isInteger(value) || value <= 0) throw new Error(`session-title: ${name} must be a positive integer`);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Convert title projection state into an immutable snapshot.
|
|
118
|
+
* @param state - the title unit's folded state.
|
|
119
|
+
* @returns the immutable snapshot.
|
|
120
|
+
*/
|
|
121
|
+
function titleSnapshotFromState(state) {
|
|
122
|
+
return deepFreeze({
|
|
123
|
+
title: state.title,
|
|
124
|
+
messageSeqs: [...state.messageSeqs],
|
|
125
|
+
source: copySessionTitleSource(state.source),
|
|
126
|
+
eventSeq: state.eventSeq,
|
|
127
|
+
updatedAt: state.updatedAt
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const EMPTY_TITLE_INPUT = {
|
|
131
|
+
first: null,
|
|
132
|
+
count: 0,
|
|
133
|
+
lastSeq: null
|
|
134
|
+
};
|
|
135
|
+
const sessionTitleUserMessageSchema = z$1.object({
|
|
136
|
+
seq: z$1.number().int().nonnegative(),
|
|
137
|
+
text: z$1.string()
|
|
138
|
+
}).strict();
|
|
139
|
+
const titleInputStateSchema = z$1.object({
|
|
140
|
+
first: sessionTitleUserMessageSchema.nullable(),
|
|
141
|
+
count: z$1.number().int().nonnegative(),
|
|
142
|
+
lastSeq: z$1.number().int().nonnegative().nullable()
|
|
143
|
+
}).strict().superRefine((state, context) => {
|
|
144
|
+
const empty = state.first === null && state.lastSeq === null && state.count === 0;
|
|
145
|
+
const populated = state.first !== null && state.lastSeq !== null && state.count > 0 && state.first.seq <= state.lastSeq;
|
|
146
|
+
if (!empty && !populated) context.addIssue({
|
|
147
|
+
code: "custom",
|
|
148
|
+
message: "title input state must pair its count with first and last message seqs"
|
|
149
|
+
});
|
|
150
|
+
});
|
|
87
151
|
/**
|
|
88
|
-
* Collect human text
|
|
89
|
-
*
|
|
90
|
-
*
|
|
152
|
+
* Collect eligible human text messages from a session log, in seq order.
|
|
153
|
+
* The full eligible prefix is only materialized for one provider generation,
|
|
154
|
+
* so it is scanned from the log at execution time rather than retained by
|
|
155
|
+
* the O(1) `titleInput` projection.
|
|
156
|
+
* @param events - the session event log.
|
|
157
|
+
* @param throughSeq - optional inclusive upper seq bound.
|
|
91
158
|
* @returns eligible messages with exact source seqs.
|
|
92
159
|
*/
|
|
93
160
|
function collectSessionTitleMessages(events, throughSeq) {
|
|
94
161
|
const messages = [];
|
|
95
162
|
for (const event of events) {
|
|
96
163
|
if (throughSeq !== void 0 && event.seq > throughSeq) break;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (normalizeSessionTitle(text, Number.MAX_SAFE_INTEGER).length === 0) continue;
|
|
100
|
-
messages.push({
|
|
101
|
-
seq: event.seq,
|
|
102
|
-
text
|
|
103
|
-
});
|
|
164
|
+
const message = sessionTitleUserMessageOf(event);
|
|
165
|
+
if (message !== void 0) messages.push(message);
|
|
104
166
|
}
|
|
105
167
|
return messages;
|
|
106
168
|
}
|
|
169
|
+
const titleViewSchema = z$1.string().min(1).nullable();
|
|
170
|
+
/** Latest logged title text and its client view. */
|
|
171
|
+
const titleProjectionDefinition = {
|
|
172
|
+
key: "title",
|
|
173
|
+
stateVersion: 1,
|
|
174
|
+
stateSchema: titleViewSchema,
|
|
175
|
+
init: () => null,
|
|
176
|
+
apply: (state, event) => event.type === "session/title" ? event.data.title : state,
|
|
177
|
+
wire: {
|
|
178
|
+
viewSchema: titleViewSchema,
|
|
179
|
+
view: (state) => state
|
|
180
|
+
}
|
|
181
|
+
};
|
|
107
182
|
/**
|
|
108
183
|
* Fold the latest logged title without consulting mutable metadata.
|
|
109
184
|
* @param events - live or persisted session log.
|
|
@@ -112,35 +187,17 @@ function collectSessionTitleMessages(events, throughSeq) {
|
|
|
112
187
|
function foldSessionTitle(events) {
|
|
113
188
|
const event = events.findLast((item) => item.type === "session/title");
|
|
114
189
|
if (event === void 0) return void 0;
|
|
115
|
-
return
|
|
190
|
+
return titleSnapshotFromState({
|
|
116
191
|
title: event.data.title,
|
|
117
|
-
messageSeqs:
|
|
118
|
-
source:
|
|
192
|
+
messageSeqs: event.data.messageSeqs,
|
|
193
|
+
source: event.data.source,
|
|
119
194
|
eventSeq: event.seq,
|
|
120
195
|
updatedAt: event.time
|
|
121
196
|
});
|
|
122
197
|
}
|
|
123
|
-
/** Defensive copy of a logged title source (the snapshot must not alias log-owned objects). */
|
|
124
|
-
function copySessionTitleSource(source) {
|
|
125
|
-
switch (source.kind) {
|
|
126
|
-
case "fallback": return { kind: "fallback" };
|
|
127
|
-
case "provider": return {
|
|
128
|
-
kind: "provider",
|
|
129
|
-
provider: source.provider,
|
|
130
|
-
...source.model === void 0 ? {} : { model: { ...source.model } }
|
|
131
|
-
};
|
|
132
|
-
case "user": return { kind: "user" };
|
|
133
|
-
/* v8 ignore next -- closed-union exhaustiveness guard */
|
|
134
|
-
default: return assertNever(source, "SessionTitleSource");
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
/** Validate one positive integer configuration field. */
|
|
138
|
-
function assertPositiveInteger(name, value) {
|
|
139
|
-
if (!Number.isInteger(value) || value <= 0) throw new Error(`session-title: ${name} must be a positive integer`);
|
|
140
|
-
}
|
|
141
198
|
/** Log-backed title fold plus asynchronous fallback generation. */
|
|
142
199
|
var SessionTitleService = class extends Service {
|
|
143
|
-
static inject = ["sessions"];
|
|
200
|
+
static inject = ["sessions", "sessionProjections"];
|
|
144
201
|
static Config = z.object({
|
|
145
202
|
fallbackMaxWords: z.number().step(1).min(1).required(),
|
|
146
203
|
fallbackMaxBytes: z.number().step(1).min(1).required(),
|
|
@@ -174,19 +231,21 @@ var SessionTitleService = class extends Service {
|
|
|
174
231
|
await this.drain(this.inFlight);
|
|
175
232
|
this.work.clear();
|
|
176
233
|
}, "sessionTitle lifecycle");
|
|
177
|
-
ctx.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
234
|
+
ctx.sessionProjections.register(titleProjectionDefinition);
|
|
235
|
+
ctx.sessionProjections.register({
|
|
236
|
+
key: "titleInput",
|
|
237
|
+
stateVersion: 3,
|
|
238
|
+
stateSchema: titleInputStateSchema,
|
|
239
|
+
init: () => EMPTY_TITLE_INPUT,
|
|
240
|
+
apply: (state, event) => {
|
|
241
|
+
const message = sessionTitleUserMessageOf(event);
|
|
242
|
+
if (message === void 0) return state;
|
|
243
|
+
return {
|
|
244
|
+
first: state.first ?? message,
|
|
245
|
+
count: state.count + 1,
|
|
246
|
+
lastSeq: message.seq
|
|
247
|
+
};
|
|
248
|
+
}
|
|
190
249
|
});
|
|
191
250
|
ctx.on("session/event", (session, event) => {
|
|
192
251
|
switch (event.type) {
|
|
@@ -261,12 +320,11 @@ var SessionTitleService = class extends Service {
|
|
|
261
320
|
this.assertServiceActive();
|
|
262
321
|
if (this.ctx.sessions.get(session.id) !== session) throw new Error(`session "${session.id}" is not live in this store`);
|
|
263
322
|
const registration = this.registration;
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
if (registration === void 0 || registration.closing || latest === void 0) {
|
|
323
|
+
const input = this.titleInputOf(session);
|
|
324
|
+
if (registration === void 0 || registration.closing || input.lastSeq === null) {
|
|
267
325
|
const current = this.get(session);
|
|
268
|
-
const
|
|
269
|
-
if (current?.source.kind === "user" && first !==
|
|
326
|
+
const first = input.first;
|
|
327
|
+
if (current?.source.kind === "user" && first !== null) {
|
|
270
328
|
this.appendFallback(session, first);
|
|
271
329
|
signal?.throwIfAborted();
|
|
272
330
|
return this.get(session);
|
|
@@ -280,7 +338,7 @@ var SessionTitleService = class extends Service {
|
|
|
280
338
|
const work = this.activate({
|
|
281
339
|
registration,
|
|
282
340
|
revision,
|
|
283
|
-
throughSeq:
|
|
341
|
+
throughSeq: input.lastSeq
|
|
284
342
|
}, state, signal);
|
|
285
343
|
const config = session.requestHeader()?.config;
|
|
286
344
|
const route = config === void 0 ? void 0 : {
|
|
@@ -319,12 +377,12 @@ var SessionTitleService = class extends Service {
|
|
|
319
377
|
/** Schedule fallback creation and any provider cadence for one eligible event. */
|
|
320
378
|
onUserMessage(session, event) {
|
|
321
379
|
if (!this.serviceActive()) return;
|
|
322
|
-
if (event.data.source.kind !== "user" ||
|
|
380
|
+
if (event.data.source.kind !== "user" || sessionTitleUserMessageOf(event) === void 0) return;
|
|
323
381
|
if (this.get(session)?.source.kind === "user") return;
|
|
324
382
|
const registration = this.registration;
|
|
325
383
|
if (registration !== void 0 && !registration.closing) {
|
|
326
|
-
const
|
|
327
|
-
if (registration.provider.automatic === "all-prompts" || session.header.parentSession === void 0 &&
|
|
384
|
+
const count = this.titleInputOf(session).count;
|
|
385
|
+
if (registration.provider.automatic === "all-prompts" || session.header.parentSession === void 0 && count === 1 && this.get(session) === void 0) {
|
|
328
386
|
const state = this.stateFor(session);
|
|
329
387
|
state.pending = {
|
|
330
388
|
registration,
|
|
@@ -361,9 +419,9 @@ var SessionTitleService = class extends Service {
|
|
|
361
419
|
const state = session === void 0 ? void 0 : this.work.get(session);
|
|
362
420
|
const pending = state?.pending;
|
|
363
421
|
if (session === void 0 || state === void 0 || pending === void 0) return;
|
|
364
|
-
const boundary =
|
|
422
|
+
const boundary = this.ctx.sessionProjections.stateOf(session, "turnBoundary")?.lastStepBoundary;
|
|
365
423
|
const route = session.requestHeader()?.config;
|
|
366
|
-
if (boundary?.
|
|
424
|
+
if (boundary?.kind !== "start" || boundary.seq <= pending.throughSeq || route?.provider !== options.provider || route.model !== options.model) return;
|
|
367
425
|
this.startPending(session, state, pending, {
|
|
368
426
|
provider: options.provider,
|
|
369
427
|
model: options.model
|
|
@@ -494,6 +552,9 @@ var SessionTitleService = class extends Service {
|
|
|
494
552
|
}
|
|
495
553
|
return state;
|
|
496
554
|
}
|
|
555
|
+
titleInputOf(session) {
|
|
556
|
+
return this.ctx.sessionProjections.stateOf(session, "titleInput");
|
|
557
|
+
}
|
|
497
558
|
/** Queue detached service work and retain it through service disposal. */
|
|
498
559
|
defer(task) {
|
|
499
560
|
const run = Promise.resolve().then(async () => {
|
|
@@ -554,8 +615,8 @@ var SessionTitleService = class extends Service {
|
|
|
554
615
|
this.assertServiceActive();
|
|
555
616
|
const current = this.get(session);
|
|
556
617
|
if (current !== void 0) return current;
|
|
557
|
-
const
|
|
558
|
-
if (first ===
|
|
618
|
+
const first = this.titleInputOf(session).first;
|
|
619
|
+
if (first === null) return void 0;
|
|
559
620
|
const title = fallbackSessionTitle(first.text, this.config.fallbackMaxWords, this.config.fallbackMaxBytes);
|
|
560
621
|
if (title.length === 0) return void 0;
|
|
561
622
|
const state = this.stateFor(session);
|
|
@@ -581,4 +642,4 @@ var SessionTitleService = class extends Service {
|
|
|
581
642
|
}
|
|
582
643
|
};
|
|
583
644
|
//#endregion
|
|
584
|
-
export { SessionTitleInvalidError, SessionTitleProviderId, SessionTitleService, SessionTitleService as default,
|
|
645
|
+
export { SessionTitleInvalidError, SessionTitleProviderId, SessionTitleService, SessionTitleService as default, fallbackSessionTitle, foldSessionTitle, normalizeSessionTitle, titleProjectionDefinition, truncateTitleUtf8 };
|
package/lib/types/index.d.ts
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { Context, Service } from '@deepseek-ai/cordis';
|
|
6
6
|
import z from '@deepseek-ai/schemastery';
|
|
7
|
+
import { z as zod } from 'zod';
|
|
7
8
|
import type { Branded } from '@deepseek-ai/dsh-brand';
|
|
8
9
|
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session';
|
|
9
|
-
export type
|
|
10
|
-
|
|
10
|
+
export type { SessionTitleEventData, SessionTitleModelProvenance, SessionTitleSnapshot, SessionTitleSource, SessionTitleUserMessage, TitleProjection, } from './types.ts';
|
|
11
|
+
import type { SessionTitleEventData, SessionTitleModelProvenance, SessionTitleSnapshot, SessionTitleUserMessage } from './types.ts';
|
|
11
12
|
/** Identifies one session-title provider registration. */
|
|
12
13
|
export type SessionTitleProviderId = Branded<'SessionTitleProviderId'>;
|
|
13
14
|
/**
|
|
@@ -16,40 +17,7 @@ export type SessionTitleProviderId = Branded<'SessionTitleProviderId'>;
|
|
|
16
17
|
* @returns the same string with the session-title provider brand.
|
|
17
18
|
*/
|
|
18
19
|
export declare function SessionTitleProviderId(id: string): SessionTitleProviderId;
|
|
19
|
-
|
|
20
|
-
export interface SessionTitleModelProvenance {
|
|
21
|
-
/** Registered LLM provider route. */
|
|
22
|
-
readonly provider: string;
|
|
23
|
-
/** Provider model id. */
|
|
24
|
-
readonly model: string;
|
|
25
|
-
}
|
|
26
|
-
/** Durable ownership record for an accepted session title. */
|
|
27
|
-
export type SessionTitleSource = {
|
|
28
|
-
readonly kind: 'fallback';
|
|
29
|
-
} | {
|
|
30
|
-
readonly kind: 'provider';
|
|
31
|
-
readonly provider: SessionTitleProviderId;
|
|
32
|
-
readonly model?: SessionTitleModelProvenance;
|
|
33
|
-
} | {
|
|
34
|
-
/** Explicit user rename: pins the title — automatic generation stops scheduling. */
|
|
35
|
-
readonly kind: 'user';
|
|
36
|
-
};
|
|
37
|
-
/** Payload of the log-only `session/title` event. */
|
|
38
|
-
export interface SessionTitleEventData {
|
|
39
|
-
/** Normalized non-empty title text. */
|
|
40
|
-
readonly title: string;
|
|
41
|
-
/** Exact human `user/message` seqs used to derive this title; empty for an explicit user rename. */
|
|
42
|
-
readonly messageSeqs: number[];
|
|
43
|
-
/** Whether the built-in fallback, a registered provider, or the user supplied the title. */
|
|
44
|
-
readonly source: SessionTitleSource;
|
|
45
|
-
}
|
|
46
|
-
/** Latest folded title plus the title event's durable envelope facts. */
|
|
47
|
-
export interface SessionTitleSnapshot extends SessionTitleEventData {
|
|
48
|
-
/** Seq of the latest `session/title` event. */
|
|
49
|
-
readonly eventSeq: number;
|
|
50
|
-
/** Timestamp of the latest `session/title` event. */
|
|
51
|
-
readonly updatedAt: number;
|
|
52
|
-
}
|
|
20
|
+
export { fallbackSessionTitle, normalizeSessionTitle, truncateTitleUtf8 } from './normalize.ts';
|
|
53
21
|
/** Required deterministic fallback and accepted-title limits. */
|
|
54
22
|
export interface Config {
|
|
55
23
|
/** Maximum whitespace-delimited words in the built-in fallback. */
|
|
@@ -82,13 +50,6 @@ declare module '@deepseek-ai/dsh-session/types' {
|
|
|
82
50
|
export declare class SessionTitleInvalidError extends Error {
|
|
83
51
|
readonly name = "SessionTitleInvalidError";
|
|
84
52
|
}
|
|
85
|
-
/** One eligible human text message exposed to title providers. */
|
|
86
|
-
export interface SessionTitleUserMessage {
|
|
87
|
-
/** Source `user/message` event seq. */
|
|
88
|
-
readonly seq: number;
|
|
89
|
-
/** Exact concatenated text-block content. */
|
|
90
|
-
readonly text: string;
|
|
91
|
-
}
|
|
92
53
|
/** Automatic generation cadence owned by a registered provider. */
|
|
93
54
|
export type SessionTitleAutomaticMode = 'first-prompt' | 'all-prompts';
|
|
94
55
|
/** Immutable input supplied to one title-provider call. */
|
|
@@ -124,13 +85,18 @@ export interface SessionTitleProvider {
|
|
|
124
85
|
*/
|
|
125
86
|
generate(request: SessionTitleProviderRequest): Promise<SessionTitleProviderResult>;
|
|
126
87
|
}
|
|
127
|
-
/**
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
88
|
+
/** Latest logged title text and its client view. */
|
|
89
|
+
export declare const titleProjectionDefinition: {
|
|
90
|
+
key: "title";
|
|
91
|
+
stateVersion: number;
|
|
92
|
+
stateSchema: zod.ZodType<string | null, unknown, zod.core.$ZodTypeInternals<string | null, unknown>>;
|
|
93
|
+
init: () => null;
|
|
94
|
+
apply: (state: string | null, event: SessionEvent) => string | null;
|
|
95
|
+
wire: {
|
|
96
|
+
viewSchema: zod.ZodType<string | null, unknown, zod.core.$ZodTypeInternals<string | null, unknown>>;
|
|
97
|
+
view: (state: string | null) => string | null;
|
|
98
|
+
};
|
|
99
|
+
};
|
|
134
100
|
/**
|
|
135
101
|
* Fold the latest logged title without consulting mutable metadata.
|
|
136
102
|
* @param events - live or persisted session log.
|
|
@@ -203,6 +169,7 @@ export declare class SessionTitleService extends Service {
|
|
|
203
169
|
private supersede;
|
|
204
170
|
/** Return mutable work state for one session. */
|
|
205
171
|
private stateFor;
|
|
172
|
+
private titleInputOf;
|
|
206
173
|
/** Queue detached service work and retain it through service disposal. */
|
|
207
174
|
private defer;
|
|
208
175
|
/** Retain one promise until settlement for service and optional provider teardown. */
|
package/lib/types/index.js
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
import { Service } from '@deepseek-ai/cordis';
|
|
6
6
|
import z from '@deepseek-ai/schemastery';
|
|
7
7
|
import { z as zod } from 'zod';
|
|
8
|
-
import {
|
|
8
|
+
import { isAgentLoopRequest } from '@deepseek-ai/dsh-llm';
|
|
9
|
+
import { assertNever, deepFreeze } from '@deepseek-ai/dsh-util-values';
|
|
9
10
|
import { fallbackSessionTitle, normalizeSessionTitle } from "./normalize.js";
|
|
10
|
-
export { fallbackSessionTitle, normalizeSessionTitle, truncateTitleUtf8 } from "./normalize.js";
|
|
11
11
|
/**
|
|
12
12
|
* Brand a raw provider id.
|
|
13
13
|
* @param id - stable non-empty provider identifier supplied by a plugin.
|
|
@@ -16,6 +16,7 @@ export { fallbackSessionTitle, normalizeSessionTitle, truncateTitleUtf8 } from "
|
|
|
16
16
|
export function SessionTitleProviderId(id) {
|
|
17
17
|
return id;
|
|
18
18
|
}
|
|
19
|
+
export { fallbackSessionTitle, normalizeSessionTitle, truncateTitleUtf8 } from "./normalize.js";
|
|
19
20
|
/**
|
|
20
21
|
* Rejection of an explicit user title whose text normalizes to empty — the
|
|
21
22
|
* one {@link SessionTitleService.rename} failure that blames the input.
|
|
@@ -25,30 +26,110 @@ export function SessionTitleProviderId(id) {
|
|
|
25
26
|
export class SessionTitleInvalidError extends Error {
|
|
26
27
|
name = 'SessionTitleInvalidError';
|
|
27
28
|
}
|
|
29
|
+
/** Extract one eligible human text message from a session event. */
|
|
30
|
+
function sessionTitleUserMessageOf(event) {
|
|
31
|
+
if (event.type !== 'user/message' || event.data.source.kind !== 'user')
|
|
32
|
+
return undefined;
|
|
33
|
+
const content = event.data.content;
|
|
34
|
+
const text = content
|
|
35
|
+
.filter((block) => block.type === 'text')
|
|
36
|
+
.map(block => block.text)
|
|
37
|
+
.join('\n');
|
|
38
|
+
if (normalizeSessionTitle(text, Number.MAX_SAFE_INTEGER).length === 0)
|
|
39
|
+
return undefined;
|
|
40
|
+
return { seq: event.seq, text };
|
|
41
|
+
}
|
|
42
|
+
/** Defensive copy of a logged title source (the snapshot must not alias log-owned objects). */
|
|
43
|
+
function copySessionTitleSource(source) {
|
|
44
|
+
switch (source.kind) {
|
|
45
|
+
case 'fallback': return { kind: 'fallback' };
|
|
46
|
+
case 'provider': return {
|
|
47
|
+
kind: 'provider',
|
|
48
|
+
provider: source.provider,
|
|
49
|
+
...(source.model === undefined ? {} : { model: { ...source.model } }),
|
|
50
|
+
};
|
|
51
|
+
case 'user': return { kind: 'user' };
|
|
52
|
+
/* v8 ignore next -- closed-union exhaustiveness guard */
|
|
53
|
+
default: return assertNever(source, 'SessionTitleSource');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Validate one positive integer configuration field. */
|
|
57
|
+
function assertPositiveInteger(name, value) {
|
|
58
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
59
|
+
throw new Error(`session-title: ${name} must be a positive integer`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
28
62
|
/**
|
|
29
|
-
*
|
|
30
|
-
* @param
|
|
31
|
-
* @
|
|
63
|
+
* Convert title projection state into an immutable snapshot.
|
|
64
|
+
* @param state - the title unit's folded state.
|
|
65
|
+
* @returns the immutable snapshot.
|
|
66
|
+
*/
|
|
67
|
+
function titleSnapshotFromState(state) {
|
|
68
|
+
return deepFreeze({
|
|
69
|
+
title: state.title,
|
|
70
|
+
messageSeqs: [...state.messageSeqs],
|
|
71
|
+
source: copySessionTitleSource(state.source),
|
|
72
|
+
eventSeq: state.eventSeq,
|
|
73
|
+
updatedAt: state.updatedAt,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
const EMPTY_TITLE_INPUT = { first: null, count: 0, lastSeq: null };
|
|
77
|
+
const sessionTitleUserMessageSchema = zod.object({
|
|
78
|
+
seq: zod.number().int().nonnegative(),
|
|
79
|
+
text: zod.string(),
|
|
80
|
+
}).strict();
|
|
81
|
+
const titleInputStateSchema = zod.object({
|
|
82
|
+
first: sessionTitleUserMessageSchema.nullable(),
|
|
83
|
+
count: zod.number().int().nonnegative(),
|
|
84
|
+
lastSeq: zod.number().int().nonnegative().nullable(),
|
|
85
|
+
}).strict().superRefine((state, context) => {
|
|
86
|
+
const empty = state.first === null && state.lastSeq === null && state.count === 0;
|
|
87
|
+
const populated = state.first !== null
|
|
88
|
+
&& state.lastSeq !== null
|
|
89
|
+
&& state.count > 0
|
|
90
|
+
&& state.first.seq <= state.lastSeq;
|
|
91
|
+
if (!empty && !populated) {
|
|
92
|
+
context.addIssue({
|
|
93
|
+
code: 'custom',
|
|
94
|
+
message: 'title input state must pair its count with first and last message seqs',
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
/**
|
|
99
|
+
* Collect eligible human text messages from a session log, in seq order.
|
|
100
|
+
* The full eligible prefix is only materialized for one provider generation,
|
|
101
|
+
* so it is scanned from the log at execution time rather than retained by
|
|
102
|
+
* the O(1) `titleInput` projection.
|
|
103
|
+
* @param events - the session event log.
|
|
104
|
+
* @param throughSeq - optional inclusive upper seq bound.
|
|
32
105
|
* @returns eligible messages with exact source seqs.
|
|
33
106
|
*/
|
|
34
|
-
|
|
107
|
+
function collectSessionTitleMessages(events, throughSeq) {
|
|
35
108
|
const messages = [];
|
|
36
109
|
for (const event of events) {
|
|
37
110
|
if (throughSeq !== undefined && event.seq > throughSeq)
|
|
38
111
|
break;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const text = content
|
|
43
|
-
.filter((block) => block.type === 'text')
|
|
44
|
-
.map(block => block.text)
|
|
45
|
-
.join('\n');
|
|
46
|
-
if (normalizeSessionTitle(text, Number.MAX_SAFE_INTEGER).length === 0)
|
|
47
|
-
continue;
|
|
48
|
-
messages.push({ seq: event.seq, text });
|
|
112
|
+
const message = sessionTitleUserMessageOf(event);
|
|
113
|
+
if (message !== undefined)
|
|
114
|
+
messages.push(message);
|
|
49
115
|
}
|
|
50
116
|
return messages;
|
|
51
117
|
}
|
|
118
|
+
const titleViewSchema = zod.string().min(1).nullable();
|
|
119
|
+
/** Latest logged title text and its client view. */
|
|
120
|
+
export const titleProjectionDefinition = {
|
|
121
|
+
key: 'title',
|
|
122
|
+
stateVersion: 1,
|
|
123
|
+
stateSchema: titleViewSchema,
|
|
124
|
+
init: () => null,
|
|
125
|
+
apply: (state, event) => (event.type === 'session/title'
|
|
126
|
+
? event.data.title
|
|
127
|
+
: state),
|
|
128
|
+
wire: {
|
|
129
|
+
viewSchema: titleViewSchema,
|
|
130
|
+
view: state => state,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
52
133
|
/**
|
|
53
134
|
* Fold the latest logged title without consulting mutable metadata.
|
|
54
135
|
* @param events - live or persisted session log.
|
|
@@ -58,37 +139,17 @@ export function foldSessionTitle(events) {
|
|
|
58
139
|
const event = events.findLast(item => item.type === 'session/title');
|
|
59
140
|
if (event === undefined)
|
|
60
141
|
return undefined;
|
|
61
|
-
return
|
|
142
|
+
return titleSnapshotFromState({
|
|
62
143
|
title: event.data.title,
|
|
63
|
-
messageSeqs:
|
|
64
|
-
source:
|
|
144
|
+
messageSeqs: event.data.messageSeqs,
|
|
145
|
+
source: event.data.source,
|
|
65
146
|
eventSeq: event.seq,
|
|
66
147
|
updatedAt: event.time,
|
|
67
148
|
});
|
|
68
149
|
}
|
|
69
|
-
/** Defensive copy of a logged title source (the snapshot must not alias log-owned objects). */
|
|
70
|
-
function copySessionTitleSource(source) {
|
|
71
|
-
switch (source.kind) {
|
|
72
|
-
case 'fallback': return { kind: 'fallback' };
|
|
73
|
-
case 'provider': return {
|
|
74
|
-
kind: 'provider',
|
|
75
|
-
provider: source.provider,
|
|
76
|
-
...(source.model === undefined ? {} : { model: { ...source.model } }),
|
|
77
|
-
};
|
|
78
|
-
case 'user': return { kind: 'user' };
|
|
79
|
-
/* v8 ignore next -- closed-union exhaustiveness guard */
|
|
80
|
-
default: return assertNever(source, 'SessionTitleSource');
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
/** Validate one positive integer configuration field. */
|
|
84
|
-
function assertPositiveInteger(name, value) {
|
|
85
|
-
if (!Number.isInteger(value) || value <= 0) {
|
|
86
|
-
throw new Error(`session-title: ${name} must be a positive integer`);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
150
|
/** Log-backed title fold plus asynchronous fallback generation. */
|
|
90
151
|
export class SessionTitleService extends Service {
|
|
91
|
-
static inject = ['sessions'];
|
|
152
|
+
static inject = ['sessions', 'sessionProjections'];
|
|
92
153
|
static Config = z.object({
|
|
93
154
|
fallbackMaxWords: z.number().step(1).min(1).required(),
|
|
94
155
|
fallbackMaxBytes: z.number().step(1).min(1).required(),
|
|
@@ -127,20 +188,22 @@ export class SessionTitleService extends Service {
|
|
|
127
188
|
await this.drain(this.inFlight);
|
|
128
189
|
this.work.clear();
|
|
129
190
|
}, 'sessionTitle lifecycle');
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
191
|
+
ctx.sessionProjections.register(titleProjectionDefinition);
|
|
192
|
+
ctx.sessionProjections.register({
|
|
193
|
+
key: 'titleInput',
|
|
194
|
+
stateVersion: 3,
|
|
195
|
+
stateSchema: titleInputStateSchema,
|
|
196
|
+
init: () => EMPTY_TITLE_INPUT,
|
|
197
|
+
apply: (state, event) => {
|
|
198
|
+
const message = sessionTitleUserMessageOf(event);
|
|
199
|
+
if (message === undefined)
|
|
200
|
+
return state;
|
|
201
|
+
return {
|
|
202
|
+
first: state.first ?? message,
|
|
203
|
+
count: state.count + 1,
|
|
204
|
+
lastSeq: message.seq,
|
|
205
|
+
};
|
|
206
|
+
},
|
|
144
207
|
});
|
|
145
208
|
ctx.on('session/event', (session, event) => {
|
|
146
209
|
switch (event.type) {
|
|
@@ -221,15 +284,14 @@ export class SessionTitleService extends Service {
|
|
|
221
284
|
throw new Error(`session "${session.id}" is not live in this store`);
|
|
222
285
|
}
|
|
223
286
|
const registration = this.registration;
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
if (registration === undefined || registration.closing || latest === undefined) {
|
|
287
|
+
const input = this.titleInputOf(session);
|
|
288
|
+
if (registration === undefined || registration.closing || input.lastSeq === null) {
|
|
227
289
|
// Explicit refresh is the unpin even without a provider: a standing
|
|
228
290
|
// user title must not short-circuit ensureFallback into a no-op, so
|
|
229
291
|
// re-derive and append the fallback over it when one is derivable.
|
|
230
292
|
const current = this.get(session);
|
|
231
|
-
const
|
|
232
|
-
if (current?.source.kind === 'user' && first !==
|
|
293
|
+
const first = input.first;
|
|
294
|
+
if (current?.source.kind === 'user' && first !== null) {
|
|
233
295
|
this.appendFallback(session, first);
|
|
234
296
|
signal?.throwIfAborted();
|
|
235
297
|
return this.get(session);
|
|
@@ -243,7 +305,7 @@ export class SessionTitleService extends Service {
|
|
|
243
305
|
const work = this.activate({
|
|
244
306
|
registration,
|
|
245
307
|
revision,
|
|
246
|
-
throughSeq:
|
|
308
|
+
throughSeq: input.lastSeq,
|
|
247
309
|
}, state, signal);
|
|
248
310
|
const config = session.requestHeader()?.config;
|
|
249
311
|
const route = config === undefined ? undefined : { provider: config.provider, model: config.model };
|
|
@@ -287,16 +349,16 @@ export class SessionTitleService extends Service {
|
|
|
287
349
|
onUserMessage(session, event) {
|
|
288
350
|
if (!this.serviceActive())
|
|
289
351
|
return;
|
|
290
|
-
if (event.data.source.kind !== 'user' ||
|
|
352
|
+
if (event.data.source.kind !== 'user' || sessionTitleUserMessageOf(event) === undefined)
|
|
291
353
|
return;
|
|
292
354
|
// A user rename pins the title: no automatic revision may override it.
|
|
293
355
|
if (this.get(session)?.source.kind === 'user')
|
|
294
356
|
return;
|
|
295
357
|
const registration = this.registration;
|
|
296
358
|
if (registration !== undefined && !registration.closing) {
|
|
297
|
-
const
|
|
359
|
+
const count = this.titleInputOf(session).count;
|
|
298
360
|
const shouldSchedule = registration.provider.automatic === 'all-prompts'
|
|
299
|
-
|| (session.header.parentSession === undefined &&
|
|
361
|
+
|| (session.header.parentSession === undefined && count === 1 && this.get(session) === undefined);
|
|
300
362
|
if (shouldSchedule) {
|
|
301
363
|
const state = this.stateFor(session);
|
|
302
364
|
const revision = this.supersede(state, 'newer user message superseded title generation');
|
|
@@ -337,9 +399,9 @@ export class SessionTitleService extends Service {
|
|
|
337
399
|
const pending = state?.pending;
|
|
338
400
|
if (session === undefined || state === undefined || pending === undefined)
|
|
339
401
|
return;
|
|
340
|
-
const boundary =
|
|
402
|
+
const boundary = this.ctx.sessionProjections.stateOf(session, 'turnBoundary')?.lastStepBoundary;
|
|
341
403
|
const route = session.requestHeader()?.config;
|
|
342
|
-
if (boundary?.
|
|
404
|
+
if (boundary?.kind !== 'start'
|
|
343
405
|
|| boundary.seq <= pending.throughSeq
|
|
344
406
|
|| route?.provider !== options.provider
|
|
345
407
|
|| route.model !== options.model)
|
|
@@ -490,6 +552,9 @@ export class SessionTitleService extends Service {
|
|
|
490
552
|
}
|
|
491
553
|
return state;
|
|
492
554
|
}
|
|
555
|
+
titleInputOf(session) {
|
|
556
|
+
return this.ctx.sessionProjections.stateOf(session, 'titleInput');
|
|
557
|
+
}
|
|
493
558
|
/** Queue detached service work and retain it through service disposal. */
|
|
494
559
|
defer(task) {
|
|
495
560
|
const run = Promise.resolve().then(async () => {
|
|
@@ -565,8 +630,8 @@ export class SessionTitleService extends Service {
|
|
|
565
630
|
const current = this.get(session);
|
|
566
631
|
if (current !== undefined)
|
|
567
632
|
return current;
|
|
568
|
-
const
|
|
569
|
-
if (first ===
|
|
633
|
+
const first = this.titleInputOf(session).first;
|
|
634
|
+
if (first === null)
|
|
570
635
|
return undefined;
|
|
571
636
|
const title = fallbackSessionTitle(first.text, this.config.fallbackMaxWords, this.config.fallbackMaxBytes);
|
|
572
637
|
if (title.length === 0)
|
package/lib/types/types.d.ts
CHANGED
|
@@ -2,15 +2,73 @@
|
|
|
2
2
|
* Pure types of the title domain: the ONE home of the `title` projection-key
|
|
3
3
|
* declaration, free of this package's host-side value imports (cordis
|
|
4
4
|
* service, schemastery, the llm seam). Two namespace projections serve it —
|
|
5
|
-
* `./types` for host consumers
|
|
6
|
-
*
|
|
5
|
+
* `./types` for host consumers and `./client/types` for client aggregates —
|
|
6
|
+
* with zero content duplication.
|
|
7
7
|
*
|
|
8
8
|
* @module @deepseek-ai/dsh-session-title/types
|
|
9
9
|
*/
|
|
10
10
|
export {};
|
|
11
|
+
import type { Branded } from '@deepseek-ai/dsh-brand';
|
|
12
|
+
/** Identifies one session-title provider registration. */
|
|
13
|
+
export type SessionTitleProviderId = Branded<'SessionTitleProviderId'>;
|
|
14
|
+
/** Exact auxiliary model route that produced a title. */
|
|
15
|
+
export interface SessionTitleModelProvenance {
|
|
16
|
+
/** Registered LLM provider route. */
|
|
17
|
+
readonly provider: string;
|
|
18
|
+
/** Provider model id. */
|
|
19
|
+
readonly model: string;
|
|
20
|
+
}
|
|
21
|
+
/** Durable ownership record for an accepted session title. */
|
|
22
|
+
export type SessionTitleSource = {
|
|
23
|
+
readonly kind: 'fallback';
|
|
24
|
+
} | {
|
|
25
|
+
readonly kind: 'provider';
|
|
26
|
+
readonly provider: SessionTitleProviderId;
|
|
27
|
+
readonly model?: SessionTitleModelProvenance;
|
|
28
|
+
} | {
|
|
29
|
+
/** Explicit user rename: pins the title — automatic generation stops scheduling. */
|
|
30
|
+
readonly kind: 'user';
|
|
31
|
+
};
|
|
32
|
+
/** Payload of the log-only `session/title` event. */
|
|
33
|
+
export interface SessionTitleEventData {
|
|
34
|
+
/** Normalized non-empty title text. */
|
|
35
|
+
readonly title: string;
|
|
36
|
+
/** Exact human `user/message` seqs used to derive this title; empty for an explicit user rename. */
|
|
37
|
+
readonly messageSeqs: number[];
|
|
38
|
+
/** Whether the built-in fallback, a registered provider, or the user supplied the title. */
|
|
39
|
+
readonly source: SessionTitleSource;
|
|
40
|
+
}
|
|
41
|
+
/** Latest folded title plus the title event's durable envelope facts. */
|
|
42
|
+
export interface SessionTitleSnapshot extends SessionTitleEventData {
|
|
43
|
+
/** Seq of the latest `session/title` event. */
|
|
44
|
+
readonly eventSeq: number;
|
|
45
|
+
/** Timestamp of the latest `session/title` event. */
|
|
46
|
+
readonly updatedAt: number;
|
|
47
|
+
}
|
|
48
|
+
/** Host title projection value. */
|
|
49
|
+
export type TitleProjection = SessionTitleSnapshot;
|
|
50
|
+
/** One eligible human text message exposed to title providers. */
|
|
51
|
+
export interface SessionTitleUserMessage {
|
|
52
|
+
/** Source `user/message` event seq. */
|
|
53
|
+
readonly seq: number;
|
|
54
|
+
/** Exact concatenated text-block content. */
|
|
55
|
+
readonly text: string;
|
|
56
|
+
}
|
|
57
|
+
/** Eligible title input stored as a bounded aggregate. */
|
|
58
|
+
export interface TitleInputState {
|
|
59
|
+
/** The oldest eligible message, or null before any. */
|
|
60
|
+
readonly first: SessionTitleUserMessage | null;
|
|
61
|
+
/** Total eligible messages folded so far. */
|
|
62
|
+
readonly count: number;
|
|
63
|
+
/** Seq of the newest eligible message, or null before any. */
|
|
64
|
+
readonly lastSeq: number | null;
|
|
65
|
+
}
|
|
11
66
|
declare module '@deepseek-ai/dsh-session-projection/types' {
|
|
12
67
|
interface SessionProjectionStateMap {
|
|
68
|
+
/** Latest logged title text, or null. */
|
|
13
69
|
title: string | null;
|
|
70
|
+
/** Eligible human title input. */
|
|
71
|
+
titleInput: TitleInputState;
|
|
14
72
|
}
|
|
15
73
|
interface SessionProjectionMap {
|
|
16
74
|
/**
|
package/lib/types/types.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Pure types of the title domain: the ONE home of the `title` projection-key
|
|
3
3
|
* declaration, free of this package's host-side value imports (cordis
|
|
4
4
|
* service, schemastery, the llm seam). Two namespace projections serve it —
|
|
5
|
-
* `./types` for host consumers
|
|
6
|
-
*
|
|
5
|
+
* `./types` for host consumers and `./client/types` for client aggregates —
|
|
6
|
+
* with zero content duplication.
|
|
7
7
|
*
|
|
8
8
|
* @module @deepseek-ai/dsh-session-title/types
|
|
9
9
|
*/
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-session-title",
|
|
3
3
|
"description": "Log-backed session title service and provider registry for the DeepSeek Harness",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.2-alpha.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -41,25 +41,29 @@
|
|
|
41
41
|
],
|
|
42
42
|
"license": "MIT",
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"@deepseek-ai/
|
|
45
|
-
"@deepseek-ai/dsh-
|
|
46
|
-
"@deepseek-ai/dsh-brand": "^0.1.
|
|
47
|
-
"@deepseek-ai/dsh-
|
|
48
|
-
"@deepseek-ai/dsh-
|
|
49
|
-
"@deepseek-ai/
|
|
44
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
45
|
+
"@deepseek-ai/dsh-agent": "^0.1.2-alpha.2",
|
|
46
|
+
"@deepseek-ai/dsh-brand": "^0.1.2-alpha.2",
|
|
47
|
+
"@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2",
|
|
48
|
+
"@deepseek-ai/dsh-llm": "^0.1.2-alpha.2",
|
|
49
|
+
"@deepseek-ai/dsh-session": "^0.1.2-alpha.2",
|
|
50
|
+
"@deepseek-ai/dsh-session-projection": "^0.1.2-alpha.2"
|
|
50
51
|
},
|
|
51
52
|
"dependencies": {
|
|
52
53
|
"zod": "^4.4.3",
|
|
53
|
-
"@deepseek-ai/
|
|
54
|
+
"@deepseek-ai/dsh-util-values": "^0.1.2-alpha.2",
|
|
55
|
+
"@deepseek-ai/schemastery": "^3.18.2"
|
|
54
56
|
},
|
|
55
57
|
"devDependencies": {
|
|
56
|
-
"@deepseek-ai/
|
|
57
|
-
"@deepseek-ai/dsh-
|
|
58
|
-
"@deepseek-ai/dsh-
|
|
59
|
-
"@deepseek-ai/dsh-
|
|
60
|
-
"@deepseek-ai/dsh-session-persistence-
|
|
61
|
-
"@deepseek-ai/
|
|
62
|
-
"@deepseek-ai/dsh-session
|
|
63
|
-
"@deepseek-ai/dsh-
|
|
58
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
59
|
+
"@deepseek-ai/dsh-agent": "^0.1.2-alpha.2",
|
|
60
|
+
"@deepseek-ai/dsh-agent-loop": "^0.1.2-alpha.2",
|
|
61
|
+
"@deepseek-ai/dsh-brand": "^0.1.2-alpha.2",
|
|
62
|
+
"@deepseek-ai/dsh-session-persistence-jsonl": "^0.1.2-alpha.2",
|
|
63
|
+
"@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2",
|
|
64
|
+
"@deepseek-ai/dsh-session": "^0.1.2-alpha.2",
|
|
65
|
+
"@deepseek-ai/dsh-llm": "^0.1.2-alpha.2",
|
|
66
|
+
"@deepseek-ai/dsh-session-projection": "^0.1.2-alpha.2",
|
|
67
|
+
"@deepseek-ai/dsh-session-persistence-sqlite": "^0.1.2-alpha.2"
|
|
64
68
|
}
|
|
65
69
|
}
|