dsh-agent-vault 1.2.0 → 1.3.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/CHANGELOG.md +18 -0
- package/README.md +2 -2
- package/client.js +50 -2
- package/docs/handoff-v2.md +13 -5
- package/handoff.js +2 -0
- package/index.js +32 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.3.0 — 2026-10-21
|
|
4
|
+
|
|
5
|
+
### 新增
|
|
6
|
+
|
|
7
|
+
- **自动开启新对话**:接力页签新增开关「检查点后自动开启新对话」。当前会话写下检查点、且该回合结束后,客户端自动新建一个会话并切过去——继承源会话的工作区、绑定、审批策略、模型与 Agent preset(模式),旧会话自动归档。
|
|
8
|
+
- 触发条件(客户端 5 秒轮询):检查点会话必须是**当前选中**的会话、已生成检查点且带有交接意图、当前没有运行中的回合;只触发一次,不会在不同会话之间乱跳。
|
|
9
|
+
- `handoff.status` 每个会话新增 `workspaceId` 与 `agentPreset`,`handoff.arm` 响应同步带 `agentPreset`,供客户端切换时定位工作区并沿用模式。
|
|
10
|
+
|
|
11
|
+
### 修复
|
|
12
|
+
|
|
13
|
+
- 续会话 / 自动切换生成的新会话会丢掉当前模式:原来回落到默认 Agent preset(如 `standard`),现在沿用源会话当前的 preset(如「创造模式」)。preset 读取 `agentPreset` 投影(实时切换后的值),旧 host 回退到 session header。
|
|
14
|
+
|
|
15
|
+
### 说明
|
|
16
|
+
|
|
17
|
+
- 自动切换是客户端行为,需要 DSH 页面/面板在运行;关闭页面就不会触发。
|
|
18
|
+
- 与手动「续会话」走同一条继承链路,只是把点击省掉了。
|
|
19
|
+
- 客户端改动需要刷新页面(或重启应用)后生效。
|
|
20
|
+
|
|
3
21
|
## 1.2.0 — 2026-10-21
|
|
4
22
|
|
|
5
23
|
### 包名
|
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ DSH 的会话本身是无状态的。跨任务时,你需要自己把上下文
|
|
|
32
32
|
| 知识库自主查 | `agent_knowledge_search` / `read` / `write` 由模型自行决定调用 |
|
|
33
33
|
| 讨论室 | 可建多个房间;用户指定哪些智能体加入,成员可中途加入或退出;每条发言标注是哪个智能体说的 |
|
|
34
34
|
| 私有日志 | 每个智能体一份详细的日记式记录,**只有它自己能读写**,按需检索,不自动注入 |
|
|
35
|
-
| 接力(检查点) | 上下文到阈值时提醒模型先写记忆和日志,两件都完成后自动把检查点写进该智能体的日志;可选额外导出 markdown;settle
|
|
35
|
+
| 接力(检查点) | 上下文到阈值时提醒模型先写记忆和日志,两件都完成后自动把检查点写进该智能体的日志;可选额外导出 markdown;settle 后新会话自动继承绑定、审批策略与模型,面板「续会话」一键续到新会话;可开启「检查点后自动开启新对话」 |
|
|
36
36
|
| 自动归档 | 续会话/新会话接管后,旧会话自动转为**归档**(只读、可查看、不能继续);面板「对话」页签可取消归档、转**强归档**(侧栏隐藏、面板灰色)或转回归档 |
|
|
37
37
|
|
|
38
38
|
## 安装
|
|
@@ -160,7 +160,7 @@ dsh plugin --profile <你的 profile> add /path/to/dsh-agent-memory
|
|
|
160
160
|
|
|
161
161
|
「接力」页签的会话列表里,处于等待沉淀或已生成检查点的会话那一行会有「重置」。它只清掉该会话的阶段记录,让它重新从 `watching` 开始;已经写进日志的检查点**不会删除**。适合手动压缩过上下文、或者想在同一会话里再走一轮的时候。
|
|
162
162
|
|
|
163
|
-
|
|
163
|
+
检查点落盘后会记一个 30 分钟内有效的「下一会话」意图:在这个目录里新建的会话会自动绑定同一智能体,并继承审批策略、模型与 Agent preset(`resume`、子智能体和已绑定的会话不会误消费)。面板的「续会话」按钮把意图刷新 + 新建会话合成一步,并让新会话落在源会话所在的工作区、沿用源会话的模式。开启「检查点后自动开启新对话」后,客户端在回合结束、会话空闲时自动完成这套切换,旧会话自动归档;API 事实与实现细节见 [docs/handoff-v2.md](docs/handoff-v2.md)。
|
|
164
164
|
|
|
165
165
|
## 面板
|
|
166
166
|
|
package/client.js
CHANGED
|
@@ -124,6 +124,8 @@ window.__ModuleLoader__.load({
|
|
|
124
124
|
handoffExportHint: '填了就额外写一份 markdown 到该目录;日志始终是唯一真相。',
|
|
125
125
|
handoffSessions: '当前会话压力',
|
|
126
126
|
handoffNoSessions: '当前没有活动会话。',
|
|
127
|
+
autoSwitch: '检查点后自动开启新对话',
|
|
128
|
+
autoSwitchHint: '当前会话写下检查点、且这个回合结束后,自动新建一个对话并切过去(继承绑定、工作区、审批策略与模型),旧会话转为归档。需要面板保持打开或页面在运行。',
|
|
127
129
|
'stage.off': '已关闭',
|
|
128
130
|
'stage.watching': '观察中',
|
|
129
131
|
'stage.due': '已到阈值',
|
|
@@ -269,6 +271,8 @@ window.__ModuleLoader__.load({
|
|
|
269
271
|
handoffExportHint: 'When set, a markdown copy is written there too; the journal stays the source of truth.',
|
|
270
272
|
handoffSessions: 'Current session pressure',
|
|
271
273
|
handoffNoSessions: 'No live sessions right now.',
|
|
274
|
+
autoSwitch: 'Start a new conversation after a checkpoint',
|
|
275
|
+
autoSwitchHint: 'Once the current session has a checkpoint and its turn is over, a fresh conversation opens automatically in the same workspace with the inherited agent, approval policy and model; the old one is archived.',
|
|
272
276
|
'stage.off': 'off',
|
|
273
277
|
'stage.watching': 'watching',
|
|
274
278
|
'stage.due': 'at threshold',
|
|
@@ -840,8 +844,12 @@ window.__ModuleLoader__.load({
|
|
|
840
844
|
setFailure(t('handoffNoWorkspace'))
|
|
841
845
|
return
|
|
842
846
|
}
|
|
843
|
-
// Inherit the source session's workspace, not whatever
|
|
844
|
-
|
|
847
|
+
// Inherit the source session's workspace and Agent preset, not whatever
|
|
848
|
+
// tab or mode was last used.
|
|
849
|
+
ui.startSession(
|
|
850
|
+
armed.workspaceId || undefined,
|
|
851
|
+
armed.agentPreset ? { agentPreset: armed.agentPreset } : undefined,
|
|
852
|
+
)
|
|
845
853
|
setHandoffVersion(item => item + 1)
|
|
846
854
|
}
|
|
847
855
|
|
|
@@ -1364,6 +1372,13 @@ window.__ModuleLoader__.load({
|
|
|
1364
1372
|
disabled: busy !== '',
|
|
1365
1373
|
onChange: checked => void saveHandoff({ enabled: checked }),
|
|
1366
1374
|
}),
|
|
1375
|
+
h(Switch, {
|
|
1376
|
+
checked: handoffInfo?.config?.autoSwitch === true,
|
|
1377
|
+
label: t('autoSwitch'),
|
|
1378
|
+
hint: t('autoSwitchHint'),
|
|
1379
|
+
disabled: busy !== '',
|
|
1380
|
+
onChange: checked => void saveHandoff({ autoSwitch: checked }),
|
|
1381
|
+
}),
|
|
1367
1382
|
h(Field, { label: t('handoffThreshold') }, h('input', {
|
|
1368
1383
|
className: 'am-input',
|
|
1369
1384
|
type: 'number',
|
|
@@ -2065,6 +2080,7 @@ window.__ModuleLoader__.load({
|
|
|
2065
2080
|
let sectionOpen = true
|
|
2066
2081
|
let lastAgents = []
|
|
2067
2082
|
let bannerSessionId = ''
|
|
2083
|
+
const autoSwitched = new Set()
|
|
2068
2084
|
let observer = null
|
|
2069
2085
|
let observedRoot = null
|
|
2070
2086
|
const collapsed = new Set()
|
|
@@ -2248,6 +2264,38 @@ window.__ModuleLoader__.load({
|
|
|
2248
2264
|
} else if (banner !== null) {
|
|
2249
2265
|
banner.remove()
|
|
2250
2266
|
}
|
|
2267
|
+
// Auto handoff: when the selected session has just written a
|
|
2268
|
+
// checkpoint and its turn is over, open a fresh conversation.
|
|
2269
|
+
if (state?.capabilities?.arm === true && selectedId !== '') {
|
|
2270
|
+
const statusPayload = await request('/op', {
|
|
2271
|
+
method: 'POST',
|
|
2272
|
+
headers: { 'content-type': 'application/json' },
|
|
2273
|
+
body: JSON.stringify({ op: 'handoff.status', args: {} }),
|
|
2274
|
+
})
|
|
2275
|
+
if (stopped) return
|
|
2276
|
+
const status = statusPayload.value
|
|
2277
|
+
if (status?.config?.autoSwitch === true) {
|
|
2278
|
+
const row = (status.sessions ?? []).find(item => item.sessionId === selectedId)
|
|
2279
|
+
const running =
|
|
2280
|
+
lastAgents.flatMap(agent => agent.sessions).find(item => item.sessionId === selectedId)?.running === true
|
|
2281
|
+
if (row?.stage === 'settled' && row.armed === true && !running && !autoSwitched.has(selectedId)) {
|
|
2282
|
+
const ui = ctx.get('uiWorkspace')
|
|
2283
|
+
if (ui !== undefined && typeof ui.startSession === 'function') {
|
|
2284
|
+
autoSwitched.add(selectedId)
|
|
2285
|
+
try {
|
|
2286
|
+
// Keep the conversation's mode: a fresh Session starts on
|
|
2287
|
+
// the default preset unless we pass the source one along.
|
|
2288
|
+
ui.startSession(
|
|
2289
|
+
row.workspaceId || undefined,
|
|
2290
|
+
row.agentPreset ? { agentPreset: row.agentPreset } : undefined,
|
|
2291
|
+
)
|
|
2292
|
+
} catch {
|
|
2293
|
+
autoSwitched.delete(selectedId)
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2251
2299
|
} catch {
|
|
2252
2300
|
/* the API may not be mounted yet */
|
|
2253
2301
|
}
|
package/docs/handoff-v2.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 接力 v2:自动续会话的设计草案
|
|
2
2
|
|
|
3
|
-
> 状态:**1.
|
|
3
|
+
> 状态:**1.3 已完成**:settle 时武装 `next` 意图,`agent/created` 自动继承绑定/审批/模型;面板「续会话」一步新建并继承源工作区;开启「检查点后自动开启新对话」后,客户端在回合结束、会话空闲时自动切到新会话(旧会话自动归档)。自动切换是客户端轮询实现(5 秒),需要页面在运行。
|
|
4
4
|
|
|
5
5
|
## 目标
|
|
6
6
|
|
|
@@ -70,10 +70,18 @@
|
|
|
70
70
|
- `handoff.status` 返回 `capabilities.arm`,旧主机(没重启)下按钮自动隐藏,不会露出必然报错的入口;
|
|
71
71
|
- 测试:`test-handoff.mjs` 覆盖一次性/cwd/过期/模型透传,`test-host.mjs` 覆盖 resume 不消费、subagent 不抢、startup 绑定 + 策略 + 模型继承 + 不复用。
|
|
72
72
|
|
|
73
|
-
##
|
|
74
|
-
|
|
75
|
-
-
|
|
76
|
-
-
|
|
73
|
+
## 自动切换(1.3 已实现)
|
|
74
|
+
|
|
75
|
+
- 接力页签开关 `autoSwitch`(handoff.json 里的布尔值,默认关);
|
|
76
|
+
- 客户端每 5 秒拉一次 `handoff.status`,触发条件全部满足才切:
|
|
77
|
+
1. `config.autoSwitch === true`;
|
|
78
|
+
2. 检查点会话就是**当前选中**的会话;
|
|
79
|
+
3. `stage === 'settled'` 且 `armed === true`(检查点写入时武装的意图还在 30 分钟有效期内);
|
|
80
|
+
4. 该会话当前没有运行中的回合(`bound.list` 的 `running` 为 false);
|
|
81
|
+
5. 同一个 session 只触发一次。
|
|
82
|
+
- 触发后 `uiWorkspace.startSession(row.workspaceId, { agentPreset: row.agentPreset })`;新会话的 `agent/created` 消费意图,继承绑定/审批/模型,并把旧会话记为归档。
|
|
83
|
+
- 模式(Agent preset)由客户端透传:`handoff.status` 每行从 `agentPreset` 投影读当前 preset(实时切换后的值,不是 header),`handoff.arm` 响应也带同一个字段,供手动「续会话」复用。
|
|
84
|
+
- 这是客户端行为:DSH 页面关掉就不会切换;也不在回合中途切,避免把正在生成的回答甩在旧会话里。
|
|
77
85
|
|
|
78
86
|
### 4. 未决问题
|
|
79
87
|
|
package/handoff.js
CHANGED
|
@@ -65,6 +65,7 @@ export function createHandoff(options = {}) {
|
|
|
65
65
|
triggerTokens: clampTokens(raw.triggerTokens, DEFAULT_TRIGGER_TOKENS),
|
|
66
66
|
remindEverySteps: Math.min(Math.max(Math.round(Number(raw.remindEverySteps) || DEFAULT_REMIND_EVERY), 1), 20),
|
|
67
67
|
exportDir: typeof raw.exportDir === 'string' ? raw.exportDir.trim() : '',
|
|
68
|
+
autoSwitch: raw.autoSwitch === true,
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
|
|
@@ -77,6 +78,7 @@ export function createHandoff(options = {}) {
|
|
|
77
78
|
next.remindEverySteps = Math.min(Math.max(Math.round(Number(patch.remindEverySteps) || current.remindEverySteps), 1), 20)
|
|
78
79
|
}
|
|
79
80
|
if (typeof patch.exportDir === 'string') next.exportDir = patch.exportDir.trim()
|
|
81
|
+
if (typeof patch.autoSwitch === 'boolean') next.autoSwitch = patch.autoSwitch
|
|
80
82
|
writeAtomic(files.config, `${JSON.stringify(next, null, 2)}\n`)
|
|
81
83
|
return next
|
|
82
84
|
}
|
package/index.js
CHANGED
|
@@ -472,6 +472,28 @@ function sessionCwd(ctx, sessionId) {
|
|
|
472
472
|
}
|
|
473
473
|
}
|
|
474
474
|
|
|
475
|
+
/**
|
|
476
|
+
* The Agent preset a Session actually runs. DSH keeps this in the
|
|
477
|
+
* `agentPreset` projection (a live preset switch appends an event without
|
|
478
|
+
* touching the immutable header), so read the projection first and only fall
|
|
479
|
+
* back to the header on older hosts.
|
|
480
|
+
*/
|
|
481
|
+
function agentPresetOf(ctx, sessionId) {
|
|
482
|
+
try {
|
|
483
|
+
const session = sessionOf(ctx, sessionId)
|
|
484
|
+
if (session === undefined) return ''
|
|
485
|
+
const projections = ctx.get('sessionProjections')
|
|
486
|
+
if (projections !== undefined && typeof projections.stateOf === 'function') {
|
|
487
|
+
const value = projections.stateOf(session, 'agentPreset')
|
|
488
|
+
if (typeof value === 'string' && value !== '') return value
|
|
489
|
+
}
|
|
490
|
+
const headerPreset = session.header?.agentPreset
|
|
491
|
+
return typeof headerPreset === 'string' ? headerPreset : ''
|
|
492
|
+
} catch {
|
|
493
|
+
return ''
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
475
497
|
function approvalOf(ctx, sessionId) {
|
|
476
498
|
try {
|
|
477
499
|
const session = sessionOf(ctx, sessionId)
|
|
@@ -699,6 +721,8 @@ function handoffStatus(ctx, store, handoff) {
|
|
|
699
721
|
tokens,
|
|
700
722
|
agentId: bound,
|
|
701
723
|
stage,
|
|
724
|
+
workspaceId: workspaceIdOf(ctx, sessionCwd(ctx, sessionId)),
|
|
725
|
+
agentPreset: agentPresetOf(ctx, sessionId),
|
|
702
726
|
requestedAt: state?.requestedAt,
|
|
703
727
|
settledAt: state?.settledAt,
|
|
704
728
|
checkpoint: state?.checkpoint,
|
|
@@ -972,6 +996,7 @@ async function handleOp(ctx, store, rooms, journals, handoff, archives, op, args
|
|
|
972
996
|
triggerTokens: args.triggerTokens,
|
|
973
997
|
remindEverySteps: args.remindEverySteps,
|
|
974
998
|
exportDir: args.exportDir,
|
|
999
|
+
autoSwitch: args.autoSwitch,
|
|
975
1000
|
})
|
|
976
1001
|
case 'handoff.reset':
|
|
977
1002
|
handoff.clearState(args.sessionId)
|
|
@@ -987,7 +1012,13 @@ async function handleOp(ctx, store, rooms, journals, handoff, archives, op, args
|
|
|
987
1012
|
approval: approvalOf(ctx, sessionId),
|
|
988
1013
|
model: modelOf(ctx, sessionId),
|
|
989
1014
|
})
|
|
990
|
-
return {
|
|
1015
|
+
return {
|
|
1016
|
+
sessionId,
|
|
1017
|
+
agentId,
|
|
1018
|
+
workspaceId: workspaceIdOf(ctx, cwd),
|
|
1019
|
+
agentPreset: agentPresetOf(ctx, sessionId),
|
|
1020
|
+
next,
|
|
1021
|
+
}
|
|
991
1022
|
}
|
|
992
1023
|
case 'workspace.list':
|
|
993
1024
|
return { workspaces: listWorkspaces(ctx) }
|