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 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.14'
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.14'
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.14",
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",