dsh-edit-turn 0.2.15 → 0.2.16
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 +8 -4
- package/lib/client.js +1 -1
- package/lib/index.js +42 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -114,11 +114,13 @@ session.append('developer/message', { turn, step, message: { role: 'developer',
|
|
|
114
114
|
1. **prompt 编辑保持单节点窗口**:`planRollback` 的 `mode === 'prompt'` 只遮蔽目标那一格(`shadowed = [target.seq]`),不会顺手回退到末尾;
|
|
115
115
|
2. **替换事件与目标同事件类型**:改 `user/message` 落下的就是 `user/message`(回复编辑不换类型就换不了位,见第 5 条);
|
|
116
116
|
3. **`sourceEventSeqs` 永远列全窗口**:官方校验要求完整覆盖,缺一个节点事件会被拒;
|
|
117
|
-
4. **每个替换都带语义标记**:`source.
|
|
117
|
+
4. **每个替换都带语义标记**:`source.editedBy === 'dsh-edit-turn'`——包括那个不含文本的空 `developer/message` 载体,兄弟插件按 `editedBy` 识别即可,不必靠推断窗口形状;
|
|
118
118
|
5. **回复编辑 = 多节点回退 + 追加新回复**,不是就地替换:旧回复行没有入口是**设计如此**(新行自带入口)。不要为了保住旧行的入口往 `assistant/message` 上加 `sourceEventSeqs`——官方校验会直接拒;
|
|
119
119
|
6. **映射已公开**:`GET /dsh-edit-turn/state` 的 `revisions[]` 给出 `{ replacementSeq, startSeq, endSeq, shadowed }`(字段名与 `POST /apply` 响应一致),`hidden[]` 每题一格列出 `{ seq, turn, replacement }`。跨插件不必再从事件流自行推导。
|
|
120
120
|
|
|
121
|
-
**`source.kind`
|
|
121
|
+
**`source.kind` 的行为(这里修过一个坑)**:prompt 编辑落地的那条 `user/message` **保持 `source.kind === 'user'`**,来历记在 `source.editedBy` 上。原因:平台里有近二十处按 `source.kind === 'user'` 认定「这是人类提问」——`dsh-session-turn-outline`(轮次导轨与轨迹视图)、`dsh-client-ui-trajectory`、`dsh-client-ui-chat`、inbox 的 steering 过滤、会话列表的 `lastPromptAt`。载体站在用户的位置上,改掉这个 kind 会让这些消费者**看不见这一轮的提问**:导轨空掉、`turnOutline` 的 `prompt` 变成空串、轨迹视图把提问归到「上下文」而不是「用户」。(0.2.15 及更早就是这样。)
|
|
122
|
+
|
|
123
|
+
保持 `kind === 'user'` **不会**让它变成「被回答的新输入」:入队根本不看 kind——inbox 投影的 reducer 只处理 `agent/inbox/spliced`(`dsh-agent-loop`),而该事件只由显式的 agent splice 写入;跳过新输入的那些扫描判的是 `surfaceOp === 'append'`,而载体是 `surfaceOp: { op: 'replace' }`。
|
|
122
124
|
|
|
123
125
|
### 验证状态
|
|
124
126
|
|
|
@@ -479,11 +481,13 @@ The transcript only builds rows for **append surface events** (ui-chat's user an
|
|
|
479
481
|
1. **A prompt edit keeps a single-node window**: `planRollback` for `mode === 'prompt'` shadows exactly the message itself (`shadowed = [target.seq]`) and never rolls back to the tail;
|
|
480
482
|
2. **The replacement event has the type of the target**: a `user/message` edit lands a `user/message` (a reply cannot be swapped in place — see 5);
|
|
481
483
|
3. **`sourceEventSeqs` always lists the whole window**: the official validator demands complete coverage; one missing node and the append is refused;
|
|
482
|
-
4. **Every replacement carries the semantic marker**: `source.
|
|
484
|
+
4. **Every replacement carries the semantic marker**: `source.editedBy === 'dsh-edit-turn'` — including the empty text-free `developer/message` carrier, so siblings can recognise rewrites by `editedBy` alone instead of inferring from the window shape;
|
|
483
485
|
5. **A reply edit is a multi-node rollback plus an appended correction**, not an in-place swap: the old reply row keeping no entry is **by design** (the new row carries it). Do not add `sourceEventSeqs` to an `assistant/message` to keep the old row's entry — the official validator refuses it outright;
|
|
484
486
|
6. **The mapping is published**: `revisions[]` on `GET /dsh-edit-turn/state` gives `{ replacementSeq, startSeq, endSeq, shadowed }` (the field names the `POST /apply` response already used), and `hidden[]` lists `{ seq, turn, replacement }` per shadowed row. No sibling has to re-derive the ledger from the event stream.
|
|
485
487
|
|
|
486
|
-
**The `source.kind` behaviour
|
|
488
|
+
**The `source.kind` behaviour — this one was a bug:** after a prompt edit the landed `user/message` **keeps `source.kind === 'user'`**, with the provenance on `source.editedBy`. About twenty places in DSH detect human prompts with `source.kind === 'user'` — `dsh-session-turn-outline` (the turn rail and the Trajectory view), `dsh-client-ui-trajectory`, `dsh-client-ui-chat`, the inbox steering filter, `lastPromptAt` on the session list. The carrier stands in the user's place, so changing that kind made every one of those consumers stop seeing the turn's prompt: the rail lost it, `turnOutline` reported an empty prompt, and the Trajectory view filed the wording under context rather than under the user. (0.2.15 and earlier did exactly that.)
|
|
489
|
+
|
|
490
|
+
Keeping `kind === 'user'` does **not** turn the carrier into fresh input that gets answered: queueing never reads `kind` — the inbox projection's reducer handles only `agent/inbox/spliced` (`dsh-agent-loop`), and that event is written solely by an explicit agent splice; the scans that skip past fresh input test `surfaceOp === 'append'`, and this carrier is `surfaceOp: { op: 'replace' }`.
|
|
487
491
|
|
|
488
492
|
### Verification status
|
|
489
493
|
|
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.16'
|
|
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.16'
|
|
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
|
|
@@ -248,16 +248,34 @@ export function messageIdOf(event) {
|
|
|
248
248
|
* @param event - raw session event.
|
|
249
249
|
* @returns true for a human prompt.
|
|
250
250
|
*/
|
|
251
|
+
export function isEditCarrier(source) {
|
|
252
|
+
if (!source || typeof source !== 'object') return false
|
|
253
|
+
// Current shape. `editedBy` is what identifies a carrier now; the prompt carrier
|
|
254
|
+
// deliberately keeps `kind: 'user'` (see buildCarrier) so the platform still reads
|
|
255
|
+
// it as the human prompt, and the silent reply carrier keeps a producer-owned kind.
|
|
256
|
+
if (source.editedBy === PLUGIN_ID) return true
|
|
257
|
+
// Historical shapes, kept so ledgers and editability stay correct over logs
|
|
258
|
+
// written by earlier releases: the producer-owned kind without `editedBy`, and
|
|
259
|
+
// the session-format v3 `{ kind: 'plugin', plugin }`.
|
|
260
|
+
if (source.kind === `plugin:${PLUGIN_ID}`) return true
|
|
261
|
+
return source.kind === 'plugin' && source.plugin === PLUGIN_ID
|
|
262
|
+
}
|
|
263
|
+
|
|
251
264
|
export function isHumanPrompt(event) {
|
|
252
265
|
if (!event || event.type !== 'user/message') return false
|
|
253
266
|
const data = event.data
|
|
254
267
|
if (!data || !data.source) return false
|
|
268
|
+
// The platform's own test, and the one every consumer applies (`turn-outline`,
|
|
269
|
+
// the Trajectory view, `ui-chat`, the inbox steering filter, `lastPromptAt`): a
|
|
270
|
+
// human prompt is a `user/message` whose source kind is `'user'`. A message this
|
|
271
|
+
// plugin rewrote stands in a human's place and keeps that kind, so it stays
|
|
272
|
+
// editable (edit it again). Matching a plugin-owned kind instead is what made
|
|
273
|
+
// the rewrite invisible to the turn outline - the round lost its prompt.
|
|
255
274
|
if (data.source.kind === 'user') return true
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
return data.source.kind === `plugin:${PLUGIN_ID}` && data.source.editedBy === PLUGIN_ID
|
|
275
|
+
// Carriers written before that fix carry a producer-owned kind. Same message -
|
|
276
|
+
// a `user/message` this plugin rewrote - so it must stay editable, or upgrading
|
|
277
|
+
// would silently make every earlier edit un-editable.
|
|
278
|
+
return isEditCarrier(data.source)
|
|
261
279
|
}
|
|
262
280
|
|
|
263
281
|
/**
|
|
@@ -304,9 +322,10 @@ export function readMessageText(event) {
|
|
|
304
322
|
/**
|
|
305
323
|
* Rebuild this plugin's rollback ledger from the log alone.
|
|
306
324
|
*
|
|
307
|
-
* Every rollback is one replacement event whose message source
|
|
308
|
-
* `{
|
|
309
|
-
* other producer - compaction, for instance - is
|
|
325
|
+
* Every rollback is one replacement event whose message source carries this
|
|
326
|
+
* plugin's marker (`{ editedBy: 'dsh-edit-turn' }`; see {@link isEditCarrier}).
|
|
327
|
+
* A replacement landed by any other producer - compaction, for instance - is
|
|
328
|
+
* ignored.
|
|
310
329
|
*
|
|
311
330
|
* @param events - complete contiguous raw event log.
|
|
312
331
|
* @returns `{ hidden, edits }`: one hidden entry per shadowed seq, and one
|
|
@@ -322,7 +341,7 @@ export function rollbackLedger(events) {
|
|
|
322
341
|
const event = bySeq.get(replacement.seq)
|
|
323
342
|
const data = event && event.data
|
|
324
343
|
const source = data && (data.source || (data.message && data.message.source))
|
|
325
|
-
if (!(source
|
|
344
|
+
if (!isEditCarrier(source)) continue
|
|
326
345
|
for (const seq of replacement.shadowed) {
|
|
327
346
|
const turn = turnOf.get(seq)
|
|
328
347
|
hidden.push({ seq, turn: typeof turn === 'number' ? turn : null, replacement: replacement.seq })
|
|
@@ -789,7 +808,19 @@ export function buildCarrier(plan, lastTurn, config) {
|
|
|
789
808
|
id: randomUUID(),
|
|
790
809
|
role: 'user',
|
|
791
810
|
content: [{ type: 'text', text: config.markerText }],
|
|
792
|
-
|
|
811
|
+
// `kind: 'user'`, NOT a plugin-owned kind. This carrier stands in a human's
|
|
812
|
+
// place - it IS the wording the user is asking with now - and the platform
|
|
813
|
+
// owns that judgement: a human prompt is exactly a `user/message` whose
|
|
814
|
+
// `source.kind` is `'user'`. Writing anything else here made the turn's
|
|
815
|
+
// prompt unrecognisable to every consumer that tests it -
|
|
816
|
+
// `dsh-session-turn-outline` (the turn rail and the Trajectory view),
|
|
817
|
+
// `dsh-client-ui-trajectory`, `dsh-client-ui-chat`, the inbox steering
|
|
818
|
+
// filter, and `lastPromptAt` on the session list - so an edited turn
|
|
819
|
+
// showed no prompt at all and `turnOutline` reported `prompt: ""`. The
|
|
820
|
+
// format's producer-owned kind (`plugin:<id>`) is for messages a plugin
|
|
821
|
+
// authors in its own voice; a prompt it merely rewrote is not one.
|
|
822
|
+
// `editedBy` below carries the provenance that `kind` used to.
|
|
823
|
+
source: { kind: 'user', editedBy: PLUGIN_ID },
|
|
793
824
|
},
|
|
794
825
|
}
|
|
795
826
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-edit-turn",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.16",
|
|
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",
|