dsh-edit-turn 0.2.14 → 0.2.15
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/README.md +6 -0
- package/lib/client.js +1 -1
- package/lib/index.js +70 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -213,6 +213,12 @@ ln -s /path/to/dsh-install/node_modules ./node_modules
|
|
|
213
213
|
|
|
214
214
|
### 更新日志
|
|
215
215
|
|
|
216
|
+
**0.2.15** —— 修一个自己埋的隐患:回复编辑开的合成轮次会让下一次提问撞号。
|
|
217
|
+
|
|
218
|
+
- **发现**:回复编辑必须在日志里开一个轮次(读路径只在"打开的轮次+步骤"里认 `assistant/message`,修正消息就是 assistant),但 **agent 循环的轮号计数器在 `phase.lastTurn`,是循环构造时播种、只由它自己开的轮推进的**——它看不见我们追加的 `turn/start`。于是下一次提问在同一个会话里会开出**同一个轮号**,两条 `turn/start` 撞号,整份日志在下次加载时报 `turn/start does not open the expected turn`,这正是我们 0.2.4 修掉的那类事故的另一个入口。兄弟插件 dsh-rerun-turn 早踩过并解决了同一问题(其 0.1.1:改用不占轮次的载体 + 事后 `syncLoopTurn`);我们照做:回复编辑闭合自己的轮次后,**把活循环的 `lastTurn` 推到该轮号**。
|
|
219
|
+
- **形状与降级**:`syncLoopTurn(agent, maxTurn)` 只看 `phase.kind === 'idle'` 且 `phase.lastTurn` 为数字时才动,否则 `'unavailable'` 什么也不碰(冷会话、循环不在场都走这条),并把结果以 `loopTurn` 写进 apply 响应、把 `unavailable` 记进 `/dsh-edit-turn/debug` 的请求环(字段刻意不叫 `code:`——静态检查会把它当宿主错误码去要文案)。
|
|
220
|
+
- 新增/改写 host 测试 2 个:命中的循环被推过消耗掉的轮号(连续两次编辑,计数器只向前),够不着的循环原样留着并留诊断;加单测 1 个(守卫:running / 缺字段 / null 都返回 unavailable)。129 单测 / 80 契约 / 静态 exit0 / verify:live / verify:ui 全绿。
|
|
221
|
+
|
|
216
222
|
**0.2.14** —— 修「重新 apply 时上一次注入的节点不清理 → 同一条消息行上堆出多个宿主」(互操作契约 I3 / §5)。只修 Bug,交互语义不变。
|
|
217
223
|
|
|
218
224
|
- **修复:重新 apply(HMR / 现场重载 / 插件开关 / bundle 组重载)会在同一行里再种一个编辑宿主,而不是复用上一次那个**。旧宿主的身份记在模块实例的 WeakMap 里,实例一换就认不出;于是每 apply 一次,那条操作条里就多一个「编辑这条消息」的笔。真机 CDP 探针实测同一 strip 里 `dshet-action-host` 多达 **7 个**、children 一度到 17(页面重载后回到 1、stripKids = 5),用户看到的是「一行上出现 3 个重跑按钮」。现在**每个注入节点都带命名空间属性**(宿主 `data-dshet-action-host="1"`、浮层根 `data-dshet-layer="1"`、编辑框 `data-dshet-editor="1"`、替代气泡 `data-dshet-revision="1"`),注入前先按属性在行内查一次、查到就**复用**——并且把它重新指到**当前**控制器(激活函数挂在按钮元素上、按下时才读,不再是上次那份捕获了旧闭包的监听;否则复用的笔点开的是已退休的控制器,表现就是「点了没反应」)。每趟 DOM pass 结束还会清掉「本次实例没有认领的」同命名空间宿主,正是那 7 个的直接来源;扫描只认自己的命名空间,兄弟插件的按钮永远不在候选里(I3)。
|
package/lib/client.js
CHANGED
|
@@ -34,7 +34,7 @@ window.__ModuleLoader__.load({
|
|
|
34
34
|
const RERUN_PREFIX = '/dsh-rerun-turn'
|
|
35
35
|
|
|
36
36
|
/** Keep in sync with package.json and lib/index.js. */
|
|
37
|
-
const PLUGIN_VERSION = '0.2.
|
|
37
|
+
const PLUGIN_VERSION = '0.2.15'
|
|
38
38
|
|
|
39
39
|
// --- copy -----------------------------------------------------------------
|
|
40
40
|
|
package/lib/index.js
CHANGED
|
@@ -57,7 +57,7 @@ export const PLUGIN_ID = 'dsh-edit-turn'
|
|
|
57
57
|
export const name = PLUGIN_ID
|
|
58
58
|
|
|
59
59
|
/** Keep in sync with package.json and lib/client.js. */
|
|
60
|
-
export const PLUGIN_VERSION = '0.2.
|
|
60
|
+
export const PLUGIN_VERSION = '0.2.15'
|
|
61
61
|
|
|
62
62
|
// A ring of the last few requests, answering "did the browser half even ask, and
|
|
63
63
|
// what did it get?" without needing its console. Read-only diagnostics: the
|
|
@@ -574,6 +574,23 @@ function findLiveSession(ctx, sessionId) {
|
|
|
574
574
|
// Resolve the live Session that owns the append. An already-open session is
|
|
575
575
|
// used directly; a cold one is resumed through the official controller, which
|
|
576
576
|
// is exactly what the web UI does when the user opens it.
|
|
577
|
+
/**
|
|
578
|
+
* The live agent whose loop owns this session's turn counter, when one exists.
|
|
579
|
+
* @param ctx - plugin context.
|
|
580
|
+
* @param sessionId - session to resolve.
|
|
581
|
+
* @returns the agent, or undefined (cold session, or no controller mounted).
|
|
582
|
+
*/
|
|
583
|
+
async function resolveLoopAgent(ctx, sessionId) {
|
|
584
|
+
const controller = ctx.get('sessionController')
|
|
585
|
+
if (!controller || typeof controller.resolveAgent !== 'function') return undefined
|
|
586
|
+
try {
|
|
587
|
+
const result = await controller.resolveAgent(sessionId)
|
|
588
|
+
return result && result.agent ? result.agent : undefined
|
|
589
|
+
} catch {
|
|
590
|
+
return undefined
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
577
594
|
async function resolveSession(ctx, sessionId) {
|
|
578
595
|
const live = findLiveSession(ctx, sessionId)
|
|
579
596
|
if (live) return live
|
|
@@ -589,6 +606,44 @@ async function resolveSession(ctx, sessionId) {
|
|
|
589
606
|
return undefined
|
|
590
607
|
}
|
|
591
608
|
|
|
609
|
+
/**
|
|
610
|
+
* Make the live agent loop agree with a turn this plugin opened in the log.
|
|
611
|
+
*
|
|
612
|
+
* The loop keeps its own counter in an idle-phase field (`phase.lastTurn`): it
|
|
613
|
+
* is seeded when the loop is constructed from the projection and advanced only
|
|
614
|
+
* by turns the loop itself opens. A reply edit has to open a turn - the read
|
|
615
|
+
* path admits an `assistant/message` only inside an open turn and step - so
|
|
616
|
+
* afterwards that field still names the turn this plugin consumed, and the
|
|
617
|
+
* loop's next run would open the same number. That collides: both turn/start
|
|
618
|
+
* events claim one number and the log fails its next load
|
|
619
|
+
* (`turn/start does not open the expected turn`), which is the corruption this
|
|
620
|
+
* plugin already shipped once and repaired.
|
|
621
|
+
*
|
|
622
|
+
* The shape guard is deliberate and the failure is soft: if the loop is not
|
|
623
|
+
* idle, or its shape is not what this expects, nothing is touched and the
|
|
624
|
+
* caller records why. The sibling plugin dsh-rerun-turn solves the same
|
|
625
|
+
* problem the same way (`syncLoopTurn`); there is no official seam for it.
|
|
626
|
+
*
|
|
627
|
+
* @param agent - the live loop agent, when one exists.
|
|
628
|
+
* @param maxTurn - the highest turn now in the log.
|
|
629
|
+
* @returns `'synced' | 'already-current' | 'unavailable'`.
|
|
630
|
+
*/
|
|
631
|
+
export function syncLoopTurn(agent, maxTurn) {
|
|
632
|
+
try {
|
|
633
|
+
const phase = agent && agent.phase
|
|
634
|
+
if (!phase || typeof phase !== 'object' || phase.kind !== 'idle' || typeof phase.lastTurn !== 'number') {
|
|
635
|
+
return 'unavailable'
|
|
636
|
+
}
|
|
637
|
+
if (maxTurn > phase.lastTurn) {
|
|
638
|
+
phase.lastTurn = maxTurn
|
|
639
|
+
return 'synced'
|
|
640
|
+
}
|
|
641
|
+
return 'already-current'
|
|
642
|
+
} catch {
|
|
643
|
+
return 'unavailable'
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
592
647
|
function eventsFromLive(session) {
|
|
593
648
|
if (session && typeof session.snapshotEvents === 'function') {
|
|
594
649
|
try {
|
|
@@ -885,11 +940,25 @@ async function applyEdit(ctx, sessionId, body, config) {
|
|
|
885
940
|
}
|
|
886
941
|
}
|
|
887
942
|
const corrected = await flushSession(ctx, session)
|
|
943
|
+
// The synthetic turn this edit opened is invisible to the live loop's own
|
|
944
|
+
// counter; without the sync its next run reuses that number and the log
|
|
945
|
+
// fails its next load. Only a reply edit opens a turn - a prompt edit does
|
|
946
|
+
// not, and needs nothing.
|
|
947
|
+
const loopAgent = await resolveLoopAgent(ctx, sessionId)
|
|
948
|
+
const loopTurn = syncLoopTurn(loopAgent, editTurn)
|
|
949
|
+
// `loopTurn` is the diagnostic the request ring records; the field is DELIBERATELY
|
|
950
|
+
// not called `code` - the static checks read `code:` as host error codes and
|
|
951
|
+
// must not start demanding a user-visible message for an internal sync state.
|
|
952
|
+
const syncNote = loopTurn
|
|
953
|
+
if (syncNote === 'unavailable') {
|
|
954
|
+
noteRequest({ kind: 'reply', sessionId, ok: true, loopTurn: syncNote, turn: editTurn })
|
|
955
|
+
}
|
|
888
956
|
return {
|
|
889
957
|
kind: 'reply',
|
|
890
958
|
replacementSeq: replacement.seq,
|
|
891
959
|
appendedSeq: appended.seq,
|
|
892
960
|
applied: true,
|
|
961
|
+
loopTurn,
|
|
893
962
|
shadowed: plan.shadowed,
|
|
894
963
|
...corrected,
|
|
895
964
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-edit-turn",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.15",
|
|
4
4
|
"description": "Edit any user turn in DeepSeek Harness and re-run it: an inline editor on the message row rolls the conversation back to just before that turn through the official surface-replace contract, then re-prompts the agent with the revised text. The append-only session log is never rewritten.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|