dsh-plugin-mobile-gateway 0.10.3 → 0.11.0
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/PROTOCOL.md +75 -2
- package/README.md +9 -1
- package/docs/agent-team-app-integration.md +945 -0
- package/docs/dsh-rc2-mobile-integration.md +10 -10
- package/docs/turn-outline-app-integration.md +196 -0
- package/lib/dsh-host-adapter.mjs +25 -5
- package/lib/index.mjs +130 -44
- package/lib/session-follower.mjs +5 -4
- package/lib/session-target.mjs +29 -0
- package/package.json +3 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# DSH 0.1
|
|
1
|
+
# DSH 0.2.1-rc.2 移动端接入
|
|
2
2
|
|
|
3
|
-
本次源码以 DSH 0.1
|
|
3
|
+
本次源码以 DSH 0.2.1-rc.2 为唯一 Host 基线,不提供旧 Host 分支。配对、设备 token、双连接和 `dsh-mobile-v1` 保持原样;以下是新的实时订阅和历史坐标约定。App 需要同步实现本说明,不能把独立流帧交给旧的 `SessionEvent.seq` reducer。
|
|
4
4
|
|
|
5
5
|
## 1. 能力与历史缓存
|
|
6
6
|
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
{
|
|
11
11
|
"kind": "hello",
|
|
12
12
|
"protocol": 3,
|
|
13
|
-
"dshVersion": "0.1
|
|
14
|
-
"historyFormatVersion":
|
|
13
|
+
"dshVersion": "0.2.1-rc.2",
|
|
14
|
+
"historyFormatVersion": 4,
|
|
15
15
|
"capabilities": ["assistant-stream-v1", "history-format-version", "projection-baseline"]
|
|
16
16
|
}
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
示例只列出新增字段和能力;原有能力仍在。`dshVersion` 是本适配器的目标版本声明,非运行时 CLI 版本探测。握手协议值 3 与 Session 格式值
|
|
19
|
+
示例只列出新增字段和能力;原有能力仍在。`dshVersion` 是本适配器的目标版本声明,非运行时 CLI 版本探测。握手协议值 3 与 Session 格式值 4 是独立概念。
|
|
20
20
|
|
|
21
21
|
缓存键至少包含 `(gatewayId, sessionId, historyFormatVersion)`。缓存没有格式版本或与服务端不同,必须丢弃该 Session 的旧历史、分页游标、fork 锚点及临时输出,重新安装基线。DSH 自己迁移磁盘日志,客户端不得迁移旧 seq。
|
|
22
22
|
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"sessionId": "s1",
|
|
43
43
|
"subscriptionId": "<UUID>",
|
|
44
44
|
"streamId": "<本次上游 follow 的 UUID>",
|
|
45
|
-
"historyFormatVersion":
|
|
45
|
+
"historyFormatVersion": 4,
|
|
46
46
|
"cursor": 41,
|
|
47
47
|
"replace": true,
|
|
48
48
|
"view": "conversation",
|
|
@@ -124,16 +124,16 @@
|
|
|
124
124
|
{ "type": "history", "sessionId": "s1", "view": "conversation" }
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
-
返回值新增 `historyFormatVersion:
|
|
127
|
+
返回值新增 `historyFormatVersion: 4`、`cursor`。conversation 视图隐藏 `system/message`、`request/header`、`request/context`,移除 Assistant 事件中的 `data.stream`,保留真实 seq、最终 message、usage 和 interrupted。默认原始视图保留完整内嵌 stream,用于按需读取轨迹。全部为隐藏事件的页仍返回可前进的 nextBeforeSeq。
|
|
128
128
|
|
|
129
129
|
携带游标必须同时携带读取该游标时的格式版本:
|
|
130
130
|
|
|
131
131
|
```json
|
|
132
|
-
{ "type": "history", "sessionId": "s1", "beforeSeq": 20, "historyFormatVersion":
|
|
133
|
-
{ "type": "fork", "sessionId": "s1", "atSeq": 42, "historyFormatVersion":
|
|
132
|
+
{ "type": "history", "sessionId": "s1", "beforeSeq": 20, "historyFormatVersion": 4 }
|
|
133
|
+
{ "type": "fork", "sessionId": "s1", "atSeq": 42, "historyFormatVersion": 4 }
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
-
缺失版本、旧版本或版本不匹配,返回 `code: "history-format-mismatch"`、`resetRequired: true` 和当前 historyFormatVersion,且不会调用上游分页/fork。客户端必须重建缓存后再取新坐标,不能给旧坐标补一个
|
|
136
|
+
缺失版本、旧版本或版本不匹配,返回 `code: "history-format-mismatch"`、`resetRequired: true` 和当前 historyFormatVersion,且不会调用上游分页/fork。客户端必须重建缓存后再取新坐标,不能给旧坐标补一个 4 后重试。游标必须是非负安全整数。省略 atSeq 的 fork 仍表示从最近完成轮次分叉。
|
|
137
137
|
|
|
138
138
|
收到 session-snapshot 后,不要用晚到的普通 history 响应覆盖正在进行的订阅基线;更早历史页只合并到历史部分。这样可以避免独立查询与实时流交错造成回滚。
|
|
139
139
|
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# App 会话轮次导航接入指南
|
|
2
|
+
|
|
3
|
+
## 接入目标
|
|
4
|
+
|
|
5
|
+
使用 DSH 的 `turnOutline` 投影,实现 WebUI 的会话刻度导航、问答预览和点击跳转。服务端提供整场会话的大纲,App 只加载当前需要的正文;无需先下载全部历史,也不依赖全文搜索或 WCDB。WCDB 可以用于后续离线缓存。
|
|
6
|
+
|
|
7
|
+
适用基线为 DSH 0.2.1-rc.2、`dsh-mobile-v1`、`hello.protocol: 3`、Session format 4。本次是向后兼容的协议扩展;App 界面和本地存储需要单独实现。
|
|
8
|
+
|
|
9
|
+
## 能力发现与数据结构
|
|
10
|
+
|
|
11
|
+
检查 `hello.capabilities` 是否包含 `turn-outline-v1`。它表示网关支持大纲快照和控制流更新的传输,不保证 Host 已挂载 `@deepseek-ai/dsh-session-turn-outline`。官方 Web profile 已组合此插件,自定义 profile 可能没有。
|
|
12
|
+
|
|
13
|
+
投影值为按 `turn` 严格递增的完整数组:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
[
|
|
17
|
+
{ "turn": 1, "seq": 10, "prompt": "调整输入框阴影", "response": "已减轻阴影,保留轻微悬浮感。" },
|
|
18
|
+
{ "turn": 2, "seq": 30, "prompt": "再缩小一点", "response": "" }
|
|
19
|
+
]
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
| 字段 | 语义 |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `turn` | Host 分配的非负轮次号;不要假定必须从 1 开始或连续 |
|
|
25
|
+
| `seq` | 该轮 `turn/start` 的持久事件序号,用于向前补齐历史 |
|
|
26
|
+
| `prompt` | 首条来源为 `user` 的人类文本预览,文本块以空格连接并折叠空白,最多 50 个 UTF-16 code unit,截断带省略号 |
|
|
27
|
+
| `response` | 最后一条有文本的助手消息预览,最多 120 个 UTF-16 code unit,在 `turn/end` 后提交 |
|
|
28
|
+
|
|
29
|
+
预览直接来自正文,不是 AI 摘要。注入上下文和工具结果不填充提示词预览;同轮后续 steering 不覆盖首条提示词。纯图片输入可以没有提示词文本,应显示“第 N 轮”等本地化占位。空回复可能表示轮次仍在运行或该轮没有助手文本,不能只据此推断生成状态。
|
|
30
|
+
|
|
31
|
+
建议共享模型中的 `turn`、`seq`、`asOfSeq` 使用 `Long`。JSON 数值按非负安全整数校验,`asOfSeq` 另允许空日志切点 `-1`。按网关 ID、会话 ID 和历史格式版本隔离缓存,不把其他网关相同 session ID 的记录混用。
|
|
32
|
+
|
|
33
|
+
## 快照与实时更新
|
|
34
|
+
|
|
35
|
+
### 当前会话快照
|
|
36
|
+
|
|
37
|
+
沿用对话连接的现有原子订阅:
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{ "type": "subscribe", "sessionId": "s1", "assistantStream": true }
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`session-snapshot` 中读取 `projections.values.turnOutline`:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"kind": "session-snapshot",
|
|
48
|
+
"sessionId": "s1",
|
|
49
|
+
"subscriptionId": "sub-1",
|
|
50
|
+
"streamId": "stream-1",
|
|
51
|
+
"replace": true,
|
|
52
|
+
"historyFormatVersion": 4,
|
|
53
|
+
"cursor": 35,
|
|
54
|
+
"events": [],
|
|
55
|
+
"hasMore": true,
|
|
56
|
+
"nextBeforeSeq": 30,
|
|
57
|
+
"projections": {
|
|
58
|
+
"asOfSeq": 35,
|
|
59
|
+
"values": {
|
|
60
|
+
"turnOutline": [
|
|
61
|
+
{ "turn": 1, "seq": 10, "prompt": "调整输入框阴影", "response": "已减轻阴影。" },
|
|
62
|
+
{ "turn": 2, "seq": 30, "prompt": "再缩小一点", "response": "" }
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"assistantStream": { "revision": 0 }
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
示例省略正文内容。`events` 和大纲是两份独立数据:首帧正文限制为最新窗口,大纲仍包含全部已开始的轮次,不随正文预算裁剪。普通 `history` 响应也保留相同的投影。
|
|
71
|
+
|
|
72
|
+
不建立实时订阅、只需要查询大纲时,可以在控制连接发送现有请求:
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{ "type": "history", "sessionId": "s1", "maxMessages": 1, "view": "conversation" }
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
这会返回少量最新正文和当前投影,不会加载全部历史。它不是无正文专用接口;不要用这样的响应替换已安装的完整聊天窗口,只提取投影即可。
|
|
79
|
+
|
|
80
|
+
### 控制连接基线
|
|
81
|
+
|
|
82
|
+
控制连接和 legacy 单连接初始化时收到 `projection-baseline`;Host 控制流重建后再次收到完整基线:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"kind": "projection-baseline",
|
|
87
|
+
"projections": {
|
|
88
|
+
"s1": {
|
|
89
|
+
"asOfSeq": 35,
|
|
90
|
+
"values": {
|
|
91
|
+
"todos": [],
|
|
92
|
+
"goal": null,
|
|
93
|
+
"turnOutline": [
|
|
94
|
+
{ "turn": 1, "seq": 10, "prompt": "调整输入框阴影", "response": "已减轻阴影。" }
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
这是 Host 当前控制流所覆盖会话的集合,通常为已附着的会话,不是磁盘上所有历史会话的目录。冷会话进入页面后仍需从自身的 `history` 或 `session-snapshot` 取得投影。基线中缺少某个 session 不表示删除该会话或其本地正文。
|
|
103
|
+
|
|
104
|
+
App 应整体替换来自控制流的基线集合。首次连接时网关可能先发送空的缓存集合,稍后再发送上游基线;不能把首个空集合视为“没有历史会话”。重建后不在集合内的旧控制状态应失效,必要时为当前页面重新取得会话快照。
|
|
105
|
+
|
|
106
|
+
### 大纲更新通知
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"kind": "turn-outline-updated",
|
|
111
|
+
"sessionId": "s1",
|
|
112
|
+
"asOfSeq": 40,
|
|
113
|
+
"turnOutline": [
|
|
114
|
+
{ "turn": 1, "seq": 10, "prompt": "调整输入框阴影", "response": "已减轻阴影。" },
|
|
115
|
+
{ "turn": 2, "seq": 30, "prompt": "再缩小一点", "response": "已缩小阴影范围。" }
|
|
116
|
+
]
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
- 通知携带完整数组,整体替换对应会话的大纲,不能 append 或按数组下标拼接。
|
|
121
|
+
- 发往控制连接和 legacy 单连接,不发往 conversation 连接,也不受该连接的 `subscribe.sessionId` 过滤。必须按 `sessionId` 分发,不能直接写入当前页面。
|
|
122
|
+
- 新控制连接的 `projection-baseline` 包含网关最近收到的大纲;其他投影更新不会覆盖它。
|
|
123
|
+
- `asOfSeq` 是上游投影对应的持久日志切点,不是独立通知流水号,不能要求连续或用它推进对话事件游标。
|
|
124
|
+
- Host 每轮通常至多更新三次大纲:开轮、首条提示词、落定回复;流式 token 不会触发大纲逐字更新。已加载轮次可以使用本地消息提供即时预览。
|
|
125
|
+
|
|
126
|
+
大纲值的清空与缺省规则:
|
|
127
|
+
|
|
128
|
+
| 值或情形 | App 行为 |
|
|
129
|
+
|---|---|
|
|
130
|
+
| `[]` | 有效的空大纲,整体替换旧数组 |
|
|
131
|
+
| `null` | 当前控制源不提供该大纲,清除其旧值,退回已加载轮次导航 |
|
|
132
|
+
| 控制基线中的投影缺失 | 网关会规范化为 `turnOutline: null` |
|
|
133
|
+
| 会话快照中该键缺失 | Host 不提供此投影,使用已加载轮次;不要伪造完整历史 |
|
|
134
|
+
| 重建基线不再包含旧 session | 控制状态失效;网关另发送该 session 的 `turn-outline-updated`,值为 `null` |
|
|
135
|
+
|
|
136
|
+
会话从控制基线消失时发出的 `null` 是失效通知,使用旧缓存的水位,不代表新的日志事件。它与新基线一起处理,不应用普通增量的水位规则忽略失效。冷会话可重新读取自己的快照恢复大纲。
|
|
137
|
+
|
|
138
|
+
## App 状态与乱序处理
|
|
139
|
+
|
|
140
|
+
建议将以下状态放在 KMM 共享层,由 Android 和 iOS 分别渲染:
|
|
141
|
+
|
|
142
|
+
- `TurnOutlineStore`:服务器大纲、来源、投影水位及连接代际。
|
|
143
|
+
- `TurnNavigationIndex`:合并服务器大纲与已加载轮次,生成有正文锚点的 `loaded` 项及只有 `seq` 的 `unloaded` 项。
|
|
144
|
+
- `TurnJumpCoordinator`:目标轮次、目标 `seq`、加载状态、可取消的跳转代际。
|
|
145
|
+
|
|
146
|
+
合并规则与 WebUI 一致:以 `turn` 为键取两侧并集并升序排列;有已加载节点则优先使用其正文锚点,非空的本地预览覆盖服务端预览,本地预览为空时回退服务端。没有可见正文节点时保留服务端 `seq`,标为未加载。渲染锚点应是稳定节点 ID,不是易变的列表下标。
|
|
147
|
+
|
|
148
|
+
在同一连接代际内,对每个会话的 `turnOutline` 单独记录 `asOfSeq`,忽略水位更小的普通快照或更新,相同水位重复值幂等处理。不要把任务或 Goal 的水位作为大纲水位:不同投影可能更新于不同事件。
|
|
149
|
+
|
|
150
|
+
收到新 `projection-baseline` 时开启新的控制基线代际并替换其集合;这是权威重建,可能使旧缓存失效。控制连接与对话连接各有代际,切网关、断线重连、订阅替换后应丢弃属于旧代际的回调。会话快照必须匹配当前 `sessionId/subscriptionId/streamId`,跨通道的旧历史响应不能覆盖新大纲或当前聊天窗口。
|
|
151
|
+
|
|
152
|
+
历史格式改变或出现 `history-format-mismatch` 时,清理该会话旧大纲的 `seq`、分页游标、跳转任务及旧格式消息缓存,再取得新基线;不能只把旧坐标的版本号改成 4 后重试。
|
|
153
|
+
|
|
154
|
+
## 点击轮次与加载历史
|
|
155
|
+
|
|
156
|
+
已加载项直接滚动到该轮用户消息;无用户节点时滚动到该轮第一个可见节点。历史分页插入头部、图片或 Markdown 改变高度时,应保持语义锚点及其偏移,避免页面跳动。
|
|
157
|
+
|
|
158
|
+
未加载项按以下步骤处理:
|
|
159
|
+
|
|
160
|
+
1. 记录目标 `turn/seq` 和新的跳转代际,暂停自动跟随底部,显示该轮的加载状态。
|
|
161
|
+
2. 复用当前历史窗口的 `nextBeforeSeq` 向前分页。请求示例:
|
|
162
|
+
|
|
163
|
+
```json
|
|
164
|
+
{ "type": "history", "sessionId": "s1", "beforeSeq": 300, "historyFormatVersion": 4, "maxMessages": 200, "view": "conversation" }
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
3. 按 `sessionId + seq` 合并、去重更早页,更新分页游标。目标 `turn/start` 进入连续补齐的历史覆盖范围后,再解析该轮消息锚点;不能仅凭本地最小 seq 判断已覆盖,因为重连缓存可能有缺口。
|
|
168
|
+
4. 未到达目标且 `hasMore: true` 时继续,必须确认 `nextBeforeSeq` 严格减小。即使可见 `events` 为空,仍使用服务端返回的有效游标继续,不以数组为空判断结束。
|
|
169
|
+
5. 布局提交后滚动到目标锚点,清除加载状态。用户再次选轮次时,只允许最新跳转执行滚动;已有历史请求可以完成并缓存,但不能把页面拉回旧目标。
|
|
170
|
+
6. 会话切换、网络错误、历史到头或游标不前进时结束循环并允许重试。未能恢复目标节点时,不显示“跳转成功”。
|
|
171
|
+
|
|
172
|
+
历史响应目前不提供独立请求 ID 的关联保证。同一会话的普通上拉加载与跳转加载需要串行协调,不要为同一 session 并发启动多个无法区分的 `history` 请求。已安装实时快照后,历史页只补充历史区域,不替换实时消息。
|
|
173
|
+
|
|
174
|
+
网关当前只开放 `maxMessages`,未开放 WebUI 内部的 `turnWindow` 参数,也没有任意锚点附近直接读取的新接口。`200` 是 App 的起始批量建议,不是协议固定值;需要服从响应实际 `hasMore/nextBeforeSeq` 和字节预算。远距离跳转可能需要多次请求。
|
|
175
|
+
|
|
176
|
+
## 触屏交互与降级
|
|
177
|
+
|
|
178
|
+
WebUI 使用固定间距刻度和虚拟列表,悬停或键盘焦点显示一行提示词与至多三行回复;点击才跳转。它在窄屏隐藏导航,App 应独立适配触屏:扩大触摸区域,按住或拖动时显示预览,松手确认跳转,并允许取消。轮次较多时虚拟化刻度,不必同时创建全部消息视图。
|
|
179
|
+
|
|
180
|
+
没有 `turn-outline-v1`、Host 缺少投影或收到 `null` 时,只展示已加载轮次。纯图片轮次使用编号;隐藏的工具轨迹不填充问答预览。书签持久化、全文搜索、智能摘要与本功能独立,不由本协议扩展提供。
|
|
181
|
+
|
|
182
|
+
完整大纲每次整值传输,大小随轮次数增长。正文预算不限制投影大小,大量会话或数千轮历史应关注客户端帧上限及内存,后续再根据实测引入大纲分页;不要把当前方案描述成恒定大小。
|
|
183
|
+
|
|
184
|
+
## 验收与源码入口
|
|
185
|
+
|
|
186
|
+
- 正文只加载尾部窗口时,仍显示全部已知轮次及问答预览。
|
|
187
|
+
- 新轮次开轮、提示词落日志、回复落定后均能更新;流式期间没有逐 token 的整大纲写入。
|
|
188
|
+
- 已加载轮次立即定位;未加载轮次连续补页到目标,期间阅读位置稳定。
|
|
189
|
+
- 连续点击不同轮次、切会话、断线重连和历史版本改变后,旧回调不能触发错误滚动。
|
|
190
|
+
- `[]` 清空,`null` 降级,控制基线缺键或移除 session 后旧控制大纲失效;会话正文和磁盘冷会话不被误删。
|
|
191
|
+
- 控制连接收到更新,对话连接只接收自己的快照;新设备恢复最近的大纲,旧客户端忽略新增字段和帧仍可对话。
|
|
192
|
+
- 中文、表情、纯图片、无助手文本、steering 和中断轮次的占位与截断正确。
|
|
193
|
+
|
|
194
|
+
网关自动测试覆盖 Host Adapter 的快照与历史分页投影保留、正文裁剪不裁掉大纲、能力发现、真实 WebSocket 的更新分流与订阅过滤、新连接恢复、空值及控制基线重建。App 触屏和滚动验收需在客户端实现后执行。
|
|
195
|
+
|
|
196
|
+
DSH 0.2.1-rc.2 的源码入口:`dsh-session-turn-outline` 的 `projection` 定义;`dsh-client-ui-chat` 的 `TurnNavigator`、`mergeTurnRailItems`、`ChatNavigation`;`dsh-api-session-controller` 的 `Session.loadThrough`。网关对应 [Host Adapter](../lib/dsh-host-adapter.mjs)、[控制流处理](../lib/index.mjs) 和 [协议说明](../PROTOCOL.md)。
|
package/lib/dsh-host-adapter.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import crypto from 'node:crypto'
|
|
2
2
|
import os from 'node:os'
|
|
3
|
+
import { sessionAddress } from './session-target.mjs'
|
|
3
4
|
|
|
4
5
|
function requireRecord(value, endpoint) {
|
|
5
6
|
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
@@ -105,12 +106,13 @@ export function createDshHostAdapter(typertGateway, peer) {
|
|
|
105
106
|
requestArgs(request),
|
|
106
107
|
signal,
|
|
107
108
|
), 'session/follow')
|
|
108
|
-
return readSessionSnapshot(frame, request.address.
|
|
109
|
+
return readSessionSnapshot(frame, request.address.kind === 'subagent'
|
|
110
|
+
? request.address.childSessionId : request.address.sessionId)
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
const history = async (payload, signal) => {
|
|
112
114
|
const request = {
|
|
113
|
-
address:
|
|
115
|
+
address: sessionAddress(payload),
|
|
114
116
|
// Older-page requests need only the opening cursor/projections, not another large latest page.
|
|
115
117
|
...(payload.beforeSeq !== undefined ? { maxMessages: 1 }
|
|
116
118
|
: payload.maxMessages === undefined ? {} : { maxMessages: payload.maxMessages }),
|
|
@@ -216,10 +218,22 @@ export function createDshHostAdapter(typertGateway, peer) {
|
|
|
216
218
|
transcribe: (request, signal) => invoke('speech', 'transcribe', { request }, signal),
|
|
217
219
|
},
|
|
218
220
|
sessions: {
|
|
221
|
+
projections: (payload, signal) => invoke('session', 'projections', requestArgs(payload), signal),
|
|
219
222
|
list: (payload = {}, signal) => invoke('session', 'list', sessionListArgs(payload), signal),
|
|
220
223
|
search: (payload, signal) => invoke('session', 'search', requestArgs(payload), signal),
|
|
221
224
|
create: (payload, signal) => invoke('session', 'create', requestArgs(payload), signal),
|
|
222
225
|
prompt: (payload, signal) => serializeSession(payload.sessionId, async () => {
|
|
226
|
+
if (payload.parentSessionId !== undefined) {
|
|
227
|
+
if (payload.subagentMode !== 'continuable') {
|
|
228
|
+
throw Object.assign(new Error('only continuable subagents accept messages'), { code: 'subagent/not-resumable' })
|
|
229
|
+
}
|
|
230
|
+
return invoke('subagents', 'prompt', requestArgs({
|
|
231
|
+
requestId: payload.requestId ?? crypto.randomUUID(),
|
|
232
|
+
parentSessionId: payload.parentSessionId, childSessionId: payload.sessionId,
|
|
233
|
+
mode: 'continuable', delivery: payload.mode, content: payload.content,
|
|
234
|
+
...(payload.clientTimeZone === undefined ? {} : { clientTimeZone: payload.clientTimeZone }),
|
|
235
|
+
}), signal)
|
|
236
|
+
}
|
|
223
237
|
// Accepted input may not have reached turn/start yet. Keep that gap
|
|
224
238
|
// locked; the persistent event or a subsequent read retires the marker.
|
|
225
239
|
const alreadyAdmitted = admittedPrompts.has(payload.sessionId)
|
|
@@ -233,7 +247,13 @@ export function createDshHostAdapter(typertGateway, peer) {
|
|
|
233
247
|
}),
|
|
234
248
|
attachment: (payload, signal) => invoke('session', 'attachment', requestArgs(payload), signal),
|
|
235
249
|
fork: (payload, signal) => invoke('session', 'fork', requestArgs(payload), signal),
|
|
236
|
-
cancel: (payload, signal) =>
|
|
250
|
+
cancel: (payload, signal) => payload.parentSessionId === undefined
|
|
251
|
+
? invoke('session', 'cancel', requestArgs(payload), signal)
|
|
252
|
+
: payload.subagentMode !== 'continuable'
|
|
253
|
+
? Promise.reject(Object.assign(new Error('only continuable subagents accept interrupts'), { code: 'subagent/not-resumable' }))
|
|
254
|
+
: invoke('subagents', 'interruptByParent', {
|
|
255
|
+
parentSessionId: payload.parentSessionId, childSessionId: payload.sessionId, mode: 'continuable',
|
|
256
|
+
}, signal),
|
|
237
257
|
updateQueue: (payload, signal) => invoke('session', 'updateQueue', requestArgs(payload), signal),
|
|
238
258
|
rename: (payload, signal) => invoke('session', 'rename', requestArgs(payload), signal),
|
|
239
259
|
selectModel: (payload, signal) => invoke('session', 'selectModel', requestArgs(payload), signal),
|
|
@@ -377,10 +397,10 @@ export function createDshHostAdapter(typertGateway, peer) {
|
|
|
377
397
|
observeSessionEvent(sessionId, event) {
|
|
378
398
|
if (event.type === 'turn/start') admittedPrompts.delete(sessionId)
|
|
379
399
|
},
|
|
380
|
-
openSessionStream(sessionId, signal) {
|
|
400
|
+
openSessionStream(sessionId, signal, target = { sessionId }) {
|
|
381
401
|
return stream('session', 'follow',
|
|
382
402
|
// Bound the opening window at the Host; older records remain available via session.page.
|
|
383
|
-
requestArgs({ address:
|
|
403
|
+
requestArgs({ address: sessionAddress(target), maxMessages: 12, assistantStream: true }),
|
|
384
404
|
signal,
|
|
385
405
|
)
|
|
386
406
|
},
|