dsh-retrace 0.4.6 → 0.4.8

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.
@@ -121,9 +121,13 @@ return {
121
121
  content: [],
122
122
  source: { kind: 'model', provider: model.provider, model: model.model },
123
123
  }
124
+ // R2 路径一(2026-08-30 事故闭环):有打开的 step(回合中编辑)时,marker 携带
125
+ // 该 step 的 turn/step —— token-meter 配对通过,不产生 T1 违规、不刷屏。
126
+ // 无打开 step(轮次间编辑,常态)才回退 turn:null(配合 markerT1Broken 标注)。
127
+ const openStep = findOpenStep(session)
124
128
  const data = {
125
- turn: null,
126
- step: null,
129
+ turn: openStep ? openStep.turn : null,
130
+ step: openStep ? openStep.step : null,
127
131
  message: marker,
128
132
  editor: {
129
133
  targetSeq,
@@ -142,6 +146,24 @@ return {
142
146
  return session.append('assistant/message', data, { surfaceOp, sourceEventSeqs })
143
147
  }
144
148
 
149
+ /**
150
+ * 找当前打开的 step(最近一次未闭合的 step/start 的 turn/step)。
151
+ * 扫描 session.events:step/start 开、step/end 关;末尾仍开即返回。
152
+ * 无打开 step 返回 null(轮次间编辑)。
153
+ */
154
+ function findOpenStep(session) {
155
+ const events = Array.isArray(session?.events) ? session.events : []
156
+ let open = null
157
+ for (const event of events) {
158
+ if (event?.type === 'step/start') {
159
+ open = { turn: event.data?.turn, step: event.data?.step }
160
+ } else if (event?.type === 'step/end') {
161
+ open = null
162
+ }
163
+ }
164
+ return open
165
+ }
166
+
145
167
  function createEditorApi(ctx, sessions, agents, log = () => {}, hooks = {}) {
146
168
  /** One in-flight op per session; later ops wait for the earlier one. */
147
169
  const locks = new Map()
@@ -165,13 +187,34 @@ return {
165
187
  return session
166
188
  }
167
189
 
168
- function requireIdle(agent) {
169
- if (agent && typeof agent.status === 'string' && agent.status === 'running') {
170
- throw editorError(
171
- 'agent-busy',
172
- 'The agent is still responding. Stop the current reply before recalling or editing.',
173
- )
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
+ agent.cancel({ kind: 'plugin-retrace-edit' })
208
+ await agent.whenIdle()
209
+ return
210
+ } catch (error) {
211
+ throw editorError('agent-stop-failed', `Failed to stop the running reply before editing: ${String(error)}`)
212
+ }
174
213
  }
214
+ throw editorError(
215
+ 'agent-busy',
216
+ 'The agent is still responding. Stop the current reply before recalling or editing.',
217
+ )
175
218
  }
176
219
 
177
220
  /** Locate the durable seq of a user/assistant message by its stable message id. */
@@ -306,7 +349,7 @@ return {
306
349
  const sessionId = String(args?.sessionId ?? '')
307
350
  const messageId = String(args?.messageId ?? '')
308
351
  const session = requireSession(sessionId)
309
- requireIdle(agents.get(sessionId))
352
+ await ensureIdle(agents.get(sessionId))
310
353
  const seq = findMessageSeq(session, messageId)
311
354
  if (seq === -1) throw editorError('message-not-found', 'Message not found in this session.')
312
355
  const span = roundSpanFrom(session, seq)
@@ -336,7 +379,7 @@ return {
336
379
  const fromScratch = args?.fromScratch === true
337
380
  const session = requireSession(sessionId)
338
381
  const agent = agents.get(sessionId)
339
- requireIdle(agent)
382
+ await ensureIdle(agent)
340
383
  const seq = findMessageSeq(session, messageId)
341
384
  if (seq === -1) throw editorError('message-not-found', 'Message not found in this session.')
342
385
  const event = session.events[seq]
@@ -377,7 +420,7 @@ return {
377
420
  const messageId = String(args?.messageId ?? '')
378
421
  const session = requireSession(sessionId)
379
422
  const agent = agents.get(sessionId)
380
- requireIdle(agent)
423
+ await ensureIdle(agent)
381
424
  const seq = findMessageSeq(session, messageId)
382
425
  if (seq === -1) throw editorError('message-not-found', 'Message not found in this session.')
383
426
  const event = session.events[seq]
package/lib/host-core.js CHANGED
@@ -112,9 +112,13 @@ export async function appendEditorMarker(session, span, op, targetSeq, originalT
112
112
  content: [],
113
113
  source: { kind: 'model', provider: model.provider, model: model.model },
114
114
  }
115
+ // R2 路径一(2026-08-30 事故闭环):有打开的 step(回合中编辑)时,marker 携带
116
+ // 该 step 的 turn/step —— token-meter 配对通过,不产生 T1 违规、不刷屏。
117
+ // 无打开 step(轮次间编辑,常态)才回退 turn:null(配合 markerT1Broken 标注)。
118
+ const openStep = findOpenStep(session)
115
119
  const data = {
116
- turn: null,
117
- step: null,
120
+ turn: openStep ? openStep.turn : null,
121
+ step: openStep ? openStep.step : null,
118
122
  message: marker,
119
123
  editor: {
120
124
  targetSeq,
@@ -133,6 +137,24 @@ export async function appendEditorMarker(session, span, op, targetSeq, originalT
133
137
  return session.append('assistant/message', data, { surfaceOp, sourceEventSeqs })
134
138
  }
135
139
 
140
+ /**
141
+ * 找当前打开的 step(最近一次未闭合的 step/start 的 turn/step)。
142
+ * 扫描 session.events:step/start 开、step/end 关;末尾仍开即返回。
143
+ * 无打开 step 返回 null(轮次间编辑)。
144
+ */
145
+ export function findOpenStep(session) {
146
+ const events = Array.isArray(session?.events) ? session.events : []
147
+ let open = null
148
+ for (const event of events) {
149
+ if (event?.type === 'step/start') {
150
+ open = { turn: event.data?.turn, step: event.data?.step }
151
+ } else if (event?.type === 'step/end') {
152
+ open = null
153
+ }
154
+ }
155
+ return open
156
+ }
157
+
136
158
  export function createEditorApi(ctx, sessions, agents, log = () => {}, hooks = {}) {
137
159
  /** One in-flight op per session; later ops wait for the earlier one. */
138
160
  const locks = new Map()
@@ -156,13 +178,34 @@ export function createEditorApi(ctx, sessions, agents, log = () => {}, hooks = {
156
178
  return session
157
179
  }
158
180
 
159
- function requireIdle(agent) {
160
- if (agent && typeof agent.status === 'string' && agent.status === 'running') {
161
- throw editorError(
162
- 'agent-busy',
163
- 'The agent is still responding. Stop the current reply before recalling or editing.',
164
- )
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
+ agent.cancel({ kind: 'plugin-retrace-edit' })
199
+ await agent.whenIdle()
200
+ return
201
+ } catch (error) {
202
+ throw editorError('agent-stop-failed', `Failed to stop the running reply before editing: ${String(error)}`)
203
+ }
165
204
  }
205
+ throw editorError(
206
+ 'agent-busy',
207
+ 'The agent is still responding. Stop the current reply before recalling or editing.',
208
+ )
166
209
  }
167
210
 
168
211
  /** Locate the durable seq of a user/assistant message by its stable message id. */
@@ -297,7 +340,7 @@ export function createEditorApi(ctx, sessions, agents, log = () => {}, hooks = {
297
340
  const sessionId = String(args?.sessionId ?? '')
298
341
  const messageId = String(args?.messageId ?? '')
299
342
  const session = requireSession(sessionId)
300
- requireIdle(agents.get(sessionId))
343
+ await ensureIdle(agents.get(sessionId))
301
344
  const seq = findMessageSeq(session, messageId)
302
345
  if (seq === -1) throw editorError('message-not-found', 'Message not found in this session.')
303
346
  const span = roundSpanFrom(session, seq)
@@ -327,7 +370,7 @@ export function createEditorApi(ctx, sessions, agents, log = () => {}, hooks = {
327
370
  const fromScratch = args?.fromScratch === true
328
371
  const session = requireSession(sessionId)
329
372
  const agent = agents.get(sessionId)
330
- requireIdle(agent)
373
+ await ensureIdle(agent)
331
374
  const seq = findMessageSeq(session, messageId)
332
375
  if (seq === -1) throw editorError('message-not-found', 'Message not found in this session.')
333
376
  const event = session.events[seq]
@@ -368,7 +411,7 @@ export function createEditorApi(ctx, sessions, agents, log = () => {}, hooks = {
368
411
  const messageId = String(args?.messageId ?? '')
369
412
  const session = requireSession(sessionId)
370
413
  const agent = agents.get(sessionId)
371
- requireIdle(agent)
414
+ await ensureIdle(agent)
372
415
  const seq = findMessageSeq(session, messageId)
373
416
  if (seq === -1) throw editorError('message-not-found', 'Message not found in this session.')
374
417
  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.6",
4
+ "version": "0.4.8",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",