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.
- package/lib/dynamic-host.js +54 -11
- package/lib/host-core.js +54 -11
- package/package.json +1 -1
package/lib/dynamic-host.js
CHANGED
|
@@ -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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
+
"version": "0.4.8",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|