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 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
- v1 只做「沉淀」,**不会自动切换会话**。检查点落盘后会记一个 30 分钟内有效的「下一会话」意图:在这个目录里新建的会话会自动绑定同一智能体,并继承审批策略与模型(`resume`、子智能体和已绑定的会话不会误消费)。面板的「续会话」按钮把意图刷新 + 新建会话合成一步,并让新会话落在源会话所在的工作区。自动切换(不经用户点击)和旧会话自动归档仍未实现,API 事实与剩余工作见 [docs/handoff-v2.md](docs/handoff-v2.md)。
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 tab was last used.
844
- ui.startSession(armed.workspaceId || undefined)
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
  }
@@ -1,6 +1,6 @@
1
1
  # 接力 v2:自动续会话的设计草案
2
2
 
3
- > 状态:**1.1/1.2 已实现**:settle 时武装 `next` 意图,`agent/created` 自动继承绑定、审批策略与模型,面板「续会话」按钮一步新建会话并继承源会话的工作区(1.2)。**未实现**(留到下一个版本):自动切换(不经点击)、旧会话自动归档。本文件保留 API 事实与剩余工作。
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
- - **旧会话归档**:面板没有做 archive,旧会话留在列表里。
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 { sessionId, agentId, workspaceId: workspaceIdOf(ctx, cwd), next }
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) }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-agent-vault",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Per-agent memory, one shared knowledge base, and per-conversation agent roles for DeepSeek Harness",
5
5
  "type": "module",
6
6
  "exports": {