dsh-retrace 0.4.7 → 0.4.9

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.
@@ -187,13 +187,37 @@ return {
187
187
  return session
188
188
  }
189
189
 
190
- function requireIdle(agent) {
191
- if (agent && typeof agent.status === 'string' && agent.status === 'running') {
192
- throw editorError(
193
- 'agent-busy',
194
- 'The agent is still responding. Stop the current reply before recalling or editing.',
195
- )
190
+ /**
191
+ * 确保 agent 空闲(2026-08-30 事故闭环,用户方案落地):agent 正在响应时,
192
+ * **不拒绝**——自动请求停止(`agent.cancel`)并等待其干净收尾(`whenIdle`)。
193
+ *
194
+ * 为什么必须等停止:编辑/重发发生在 agent 还开着 step 时,DSH resend 机制
195
+ * 会把旧 step 的 assistant/chunk 全部引用进新 assistant/message 的
196
+ * `sourceEventSeqs`(526f1835 seq 936047:引用跨 turn 54 的 step 7/8/9),
197
+ * token-meter 对跨 step 引用抛 `belongs to another step`(dsh-token-meter
198
+ * lib/index.js:645)→ 同样刷屏压垮 host。先停止 → step 干净关闭 → 编辑在
199
+ * 轮次边界执行,不再触发跨 step 引用。
200
+ *
201
+ * agent 无 cancel/whenIdle(headless/测试桩)时回退为抛 agent-busy(原行为)。
202
+ */
203
+ async function ensureIdle(agent) {
204
+ if (!agent || typeof agent.status !== 'string' || agent.status !== 'running') return
205
+ if (typeof agent.cancel === 'function' && typeof agent.whenIdle === 'function') {
206
+ try {
207
+ // 官方 AgentCancelCause 类型只有 user/parent/hook/disposed 四种
208
+ // (dsh-commands typert.host.js:166)——用 { kind: 'user' } 与 UI 停止按钮同义,
209
+ // 中断的 turn/end 会按官方语义记录 reason。
210
+ agent.cancel({ kind: 'user' })
211
+ await agent.whenIdle()
212
+ return
213
+ } catch (error) {
214
+ throw editorError('agent-stop-failed', `Failed to stop the running reply before editing: ${String(error)}`)
215
+ }
196
216
  }
217
+ throw editorError(
218
+ 'agent-busy',
219
+ 'The agent is still responding. Stop the current reply before recalling or editing.',
220
+ )
197
221
  }
198
222
 
199
223
  /** Locate the durable seq of a user/assistant message by its stable message id. */
@@ -328,7 +352,7 @@ return {
328
352
  const sessionId = String(args?.sessionId ?? '')
329
353
  const messageId = String(args?.messageId ?? '')
330
354
  const session = requireSession(sessionId)
331
- requireIdle(agents.get(sessionId))
355
+ await ensureIdle(agents.get(sessionId))
332
356
  const seq = findMessageSeq(session, messageId)
333
357
  if (seq === -1) throw editorError('message-not-found', 'Message not found in this session.')
334
358
  const span = roundSpanFrom(session, seq)
@@ -358,7 +382,7 @@ return {
358
382
  const fromScratch = args?.fromScratch === true
359
383
  const session = requireSession(sessionId)
360
384
  const agent = agents.get(sessionId)
361
- requireIdle(agent)
385
+ await ensureIdle(agent)
362
386
  const seq = findMessageSeq(session, messageId)
363
387
  if (seq === -1) throw editorError('message-not-found', 'Message not found in this session.')
364
388
  const event = session.events[seq]
@@ -399,7 +423,7 @@ return {
399
423
  const messageId = String(args?.messageId ?? '')
400
424
  const session = requireSession(sessionId)
401
425
  const agent = agents.get(sessionId)
402
- requireIdle(agent)
426
+ await ensureIdle(agent)
403
427
  const seq = findMessageSeq(session, messageId)
404
428
  if (seq === -1) throw editorError('message-not-found', 'Message not found in this session.')
405
429
  const event = session.events[seq]
package/lib/host-core.js CHANGED
@@ -178,13 +178,37 @@ export function createEditorApi(ctx, sessions, agents, log = () => {}, hooks = {
178
178
  return session
179
179
  }
180
180
 
181
- function requireIdle(agent) {
182
- if (agent && typeof agent.status === 'string' && agent.status === 'running') {
183
- throw editorError(
184
- 'agent-busy',
185
- 'The agent is still responding. Stop the current reply before recalling or editing.',
186
- )
181
+ /**
182
+ * 确保 agent 空闲(2026-08-30 事故闭环,用户方案落地):agent 正在响应时,
183
+ * **不拒绝**——自动请求停止(`agent.cancel`)并等待其干净收尾(`whenIdle`)。
184
+ *
185
+ * 为什么必须等停止:编辑/重发发生在 agent 还开着 step 时,DSH resend 机制
186
+ * 会把旧 step 的 assistant/chunk 全部引用进新 assistant/message 的
187
+ * `sourceEventSeqs`(526f1835 seq 936047:引用跨 turn 54 的 step 7/8/9),
188
+ * token-meter 对跨 step 引用抛 `belongs to another step`(dsh-token-meter
189
+ * lib/index.js:645)→ 同样刷屏压垮 host。先停止 → step 干净关闭 → 编辑在
190
+ * 轮次边界执行,不再触发跨 step 引用。
191
+ *
192
+ * agent 无 cancel/whenIdle(headless/测试桩)时回退为抛 agent-busy(原行为)。
193
+ */
194
+ async function ensureIdle(agent) {
195
+ if (!agent || typeof agent.status !== 'string' || agent.status !== 'running') return
196
+ if (typeof agent.cancel === 'function' && typeof agent.whenIdle === 'function') {
197
+ try {
198
+ // 官方 AgentCancelCause 类型只有 user/parent/hook/disposed 四种
199
+ // (dsh-commands typert.host.js:166)——用 { kind: 'user' } 与 UI 停止按钮同义,
200
+ // 中断的 turn/end 会按官方语义记录 reason。
201
+ agent.cancel({ kind: 'user' })
202
+ await agent.whenIdle()
203
+ return
204
+ } catch (error) {
205
+ throw editorError('agent-stop-failed', `Failed to stop the running reply before editing: ${String(error)}`)
206
+ }
187
207
  }
208
+ throw editorError(
209
+ 'agent-busy',
210
+ 'The agent is still responding. Stop the current reply before recalling or editing.',
211
+ )
188
212
  }
189
213
 
190
214
  /** Locate the durable seq of a user/assistant message by its stable message id. */
@@ -319,7 +343,7 @@ export function createEditorApi(ctx, sessions, agents, log = () => {}, hooks = {
319
343
  const sessionId = String(args?.sessionId ?? '')
320
344
  const messageId = String(args?.messageId ?? '')
321
345
  const session = requireSession(sessionId)
322
- requireIdle(agents.get(sessionId))
346
+ await ensureIdle(agents.get(sessionId))
323
347
  const seq = findMessageSeq(session, messageId)
324
348
  if (seq === -1) throw editorError('message-not-found', 'Message not found in this session.')
325
349
  const span = roundSpanFrom(session, seq)
@@ -349,7 +373,7 @@ export function createEditorApi(ctx, sessions, agents, log = () => {}, hooks = {
349
373
  const fromScratch = args?.fromScratch === true
350
374
  const session = requireSession(sessionId)
351
375
  const agent = agents.get(sessionId)
352
- requireIdle(agent)
376
+ await ensureIdle(agent)
353
377
  const seq = findMessageSeq(session, messageId)
354
378
  if (seq === -1) throw editorError('message-not-found', 'Message not found in this session.')
355
379
  const event = session.events[seq]
@@ -390,7 +414,7 @@ export function createEditorApi(ctx, sessions, agents, log = () => {}, hooks = {
390
414
  const messageId = String(args?.messageId ?? '')
391
415
  const session = requireSession(sessionId)
392
416
  const agent = agents.get(sessionId)
393
- requireIdle(agent)
417
+ await ensureIdle(agent)
394
418
  const seq = findMessageSeq(session, messageId)
395
419
  if (seq === -1) throw editorError('message-not-found', 'Message not found in this session.')
396
420
  const event = session.events[seq]
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-retrace",
3
3
  "description": "Retrace · 回溯 — Recall, edit-and-resend, regenerate, and conversation/artifact versioning (timeline, rollback, fork map) for DeepSeek Harness — Web and Desktop",
4
- "version": "0.4.7",
4
+ "version": "0.4.9",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",