dsh-edit-turn 0.2.0 → 0.2.4
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 +432 -64
- package/lib/client.js +709 -98
- package/lib/index.js +247 -60
- package/package.json +6 -2
package/lib/index.js
CHANGED
|
@@ -22,25 +22,37 @@
|
|
|
22
22
|
// admission path. It resumes the Session when cold and starts exactly one
|
|
23
23
|
// new turn from the revised text.
|
|
24
24
|
//
|
|
25
|
-
// The replacement carrier
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
// model can see and the derived context after an edit is exactly
|
|
29
|
-
// be had the conversation really stopped at the edit point.
|
|
30
|
-
//
|
|
31
|
-
// an equivalent plugin already runs in production, so it is the fallback.
|
|
32
|
-
// Either way `sourceEventSeqs` must list the complete shadowed node set, which
|
|
25
|
+
// The silent replacement carrier is an EMPTY `developer/message` (0.2.4;
|
|
26
|
+
// before that an empty `system/message`, which no reader accepts - see the
|
|
27
|
+
// README). Empty content projects to no model message, so the rollback adds
|
|
28
|
+
// nothing the model can see and the derived context after an edit is exactly
|
|
29
|
+
// what it would be had the conversation really stopped at the edit point.
|
|
30
|
+
// `sourceEventSeqs` must list the complete shadowed node set, which
|
|
33
31
|
// is why an `assistant/message` carrier is impossible (it forbids
|
|
34
32
|
// `sourceEventSeqs` outright).
|
|
35
33
|
//
|
|
36
|
-
// The module
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
34
|
+
// The module resolves `@deepseek-ai/schemastery` defensively. `dsh-tools` is a
|
|
35
|
+
// host-provided peer (the loader resolves it for every plugin), but the schema
|
|
36
|
+
// package is a regular dependency: when it cannot be resolved - a pruned
|
|
37
|
+
// package cache, an installation that skipped dependencies - a static import
|
|
38
|
+
// takes the whole plugin down and the feature simply disappears from the UI
|
|
39
|
+
// with nothing but a line on the host's stdout to explain it. A guarded require
|
|
40
|
+
// degrades to "no config form, defaults apply" instead, which costs a settings
|
|
41
|
+
// surface rather than the feature. Every field is optional in `apply` anyway.
|
|
40
42
|
import { randomUUID } from 'node:crypto'
|
|
41
|
-
import
|
|
43
|
+
import { createRequire } from 'node:module'
|
|
42
44
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
43
45
|
|
|
46
|
+
const nodeRequire = createRequire(import.meta.url)
|
|
47
|
+
const schemaPackage = (() => {
|
|
48
|
+
try {
|
|
49
|
+
return nodeRequire('@deepseek-ai/schemastery')
|
|
50
|
+
} catch {
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
})()
|
|
54
|
+
const z = schemaPackage === null ? null : (schemaPackage.default ?? schemaPackage)
|
|
55
|
+
|
|
44
56
|
/** Plugin id shared by the host and browser halves. */
|
|
45
57
|
export const PLUGIN_ID = 'dsh-edit-turn'
|
|
46
58
|
|
|
@@ -48,7 +60,24 @@ export const PLUGIN_ID = 'dsh-edit-turn'
|
|
|
48
60
|
export const name = PLUGIN_ID
|
|
49
61
|
|
|
50
62
|
/** Keep in sync with package.json and lib/client.js. */
|
|
51
|
-
export const PLUGIN_VERSION = '0.2.
|
|
63
|
+
export const PLUGIN_VERSION = '0.2.4'
|
|
64
|
+
|
|
65
|
+
// A ring of the last few requests, answering "did the browser half even ask, and
|
|
66
|
+
// what did it get?" without needing its console. Read-only diagnostics: the
|
|
67
|
+
// browser half's own state is otherwise invisible from the host side.
|
|
68
|
+
const REQUEST_LOG_LIMIT = 40
|
|
69
|
+
const requestLog = []
|
|
70
|
+
|
|
71
|
+
/** Record one request for the diagnostic route. */
|
|
72
|
+
export function noteRequest(entry) {
|
|
73
|
+
requestLog.push({ at: Date.now(), ...entry })
|
|
74
|
+
if (requestLog.length > REQUEST_LOG_LIMIT) requestLog.shift()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The recorded requests, oldest first. */
|
|
78
|
+
export function recentRequests() {
|
|
79
|
+
return requestLog.slice()
|
|
80
|
+
}
|
|
52
81
|
|
|
53
82
|
const ROUTE_PREFIX = '/dsh-edit-turn'
|
|
54
83
|
const SESSION_ID_RE = /^(session-)?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
@@ -65,16 +94,32 @@ const FLUSH_DEADLINE_MS = 5_000
|
|
|
65
94
|
*/
|
|
66
95
|
const DEFAULT_MARKER = '[dsh-edit-turn] The user revised an earlier message. Every turn after that message was rolled back; the revised message follows. Send your reply to the revised message.'
|
|
67
96
|
|
|
68
|
-
/**
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Deployment policy for the rollback.
|
|
99
|
+
*
|
|
100
|
+
* `undefined` when the schema package could not be resolved - DSH then hands
|
|
101
|
+
* `apply` no config at all, and every field below is read with the same default
|
|
102
|
+
* there, so the plugin behaves identically minus the settings form.
|
|
103
|
+
*/
|
|
104
|
+
export const Config =
|
|
105
|
+
z === null
|
|
106
|
+
? undefined
|
|
107
|
+
: z.object({
|
|
108
|
+
markerText: z.string().default(DEFAULT_MARKER),
|
|
109
|
+
/**
|
|
110
|
+
* Whether the client asks for a second confirmation before applying. Off
|
|
111
|
+
* by default: the editor is already an explicit action the user opened, and
|
|
112
|
+
* the panel states what saving does. Set it to `true` to require the extra
|
|
113
|
+
* step.
|
|
114
|
+
*/
|
|
115
|
+
confirm: z.boolean().default(false),
|
|
116
|
+
/**
|
|
117
|
+
* Whether saving a revised prompt also runs the turn again. Off by default:
|
|
118
|
+
* the revision becomes the standing wording and waits for the next message,
|
|
119
|
+
* so saving costs no model call. Set it to `true` to re-run on save.
|
|
120
|
+
*/
|
|
121
|
+
rerun: z.boolean().default(false),
|
|
122
|
+
})
|
|
78
123
|
|
|
79
124
|
// ---------------------------------------------------------------------------
|
|
80
125
|
// pure session-log logic (no DSH SDK imports; unit-testable under node --test)
|
|
@@ -216,7 +261,13 @@ export function messageIdOf(event) {
|
|
|
216
261
|
export function isHumanPrompt(event) {
|
|
217
262
|
if (!event || event.type !== 'user/message') return false
|
|
218
263
|
const data = event.data
|
|
219
|
-
|
|
264
|
+
if (!data || !data.source) return false
|
|
265
|
+
if (data.source.kind === 'user') return true
|
|
266
|
+
// A message this plugin rewrote stands in a human's place: it is the wording
|
|
267
|
+
// the user is asking with now, so it stays editable (edit it again) even though
|
|
268
|
+
// its source records that the plugin wrote it. The distinct kind also keeps the
|
|
269
|
+
// platform from treating it as fresh input to answer.
|
|
270
|
+
return data.source.kind === `plugin:${PLUGIN_ID}` && data.source.editedBy === PLUGIN_ID
|
|
220
271
|
}
|
|
221
272
|
|
|
222
273
|
/**
|
|
@@ -296,6 +347,16 @@ export function rollbackLedger(events) {
|
|
|
296
347
|
return { hidden, edits }
|
|
297
348
|
}
|
|
298
349
|
|
|
350
|
+
/** The turn number the format expects next: the last one opened, plus one. */
|
|
351
|
+
export function nextTurnOf(events) {
|
|
352
|
+
let last = 0
|
|
353
|
+
for (const event of events) {
|
|
354
|
+
const turn = event.data && event.data.turn
|
|
355
|
+
if (typeof turn === 'number' && turn > last) last = turn
|
|
356
|
+
}
|
|
357
|
+
return last + 1
|
|
358
|
+
}
|
|
359
|
+
|
|
299
360
|
/** Highest turn number recorded in the log, or undefined for an empty log. */
|
|
300
361
|
export function lastTurnOf(events) {
|
|
301
362
|
let last
|
|
@@ -373,14 +434,21 @@ export function planRollback(events, surfaceNodes, request) {
|
|
|
373
434
|
if (target.seq === surfaceNodes[0]) {
|
|
374
435
|
throw new EditPlanError('not-editable', 'the system prompt head cannot be edited')
|
|
375
436
|
}
|
|
376
|
-
// Two editable kinds
|
|
377
|
-
//
|
|
437
|
+
// Two editable kinds. A prompt is rewritten in place and the conversation
|
|
438
|
+
// continues; a reply is replaced and everything built on it goes with it.
|
|
378
439
|
const mode = isHumanPrompt(target) ? 'prompt' : isAssistantReply(target) ? 'reply' : null
|
|
379
440
|
if (mode === null) {
|
|
380
441
|
throw new EditPlanError('not-editable', 'only your own messages and the model replies can be edited')
|
|
381
442
|
}
|
|
382
443
|
const startIdx = nodeIndex.get(target.seq)
|
|
383
|
-
|
|
444
|
+
// Two different windows, because the two edits mean different things.
|
|
445
|
+
//
|
|
446
|
+
// A revised PROMPT is replaced in place: only the message itself leaves the
|
|
447
|
+
// surface, so the reply under it and everything after stay exactly as they
|
|
448
|
+
// were - edit the wording, keep the conversation. A revised REPLY cannot do
|
|
449
|
+
// that (an answer invalidates whatever was built on it), so its window runs to
|
|
450
|
+
// the tail and the turn is closed again behind the correction.
|
|
451
|
+
const shadowed = mode === 'prompt' ? [target.seq] : surfaceNodes.slice(startIdx)
|
|
384
452
|
if (shadowed.length === 0 || shadowed[0] !== target.seq) {
|
|
385
453
|
throw new EditPlanError('not-editable', 'the target does not open a rollback window')
|
|
386
454
|
}
|
|
@@ -419,18 +487,22 @@ export function planRollback(events, surfaceNodes, request) {
|
|
|
419
487
|
* @returns one entry per editable prompt, in conversation order.
|
|
420
488
|
*/
|
|
421
489
|
export function editableTurns(events, surfaceNodes) {
|
|
422
|
-
const
|
|
490
|
+
const eventBySeq = new Map(events.map((event) => [event.seq, event]))
|
|
423
491
|
const turnOf = turnIndex(events)
|
|
424
492
|
const head = surfaceNodes[0]
|
|
425
493
|
const out = []
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
494
|
+
// Surface order, not log order: a prompt rewritten in place travels as a
|
|
495
|
+
// replace-carrier, which is APPENDED at the end of the log while standing at
|
|
496
|
+
// the surface position of the wording it replaced. Walking `events` would
|
|
497
|
+
// report the revision as the newest message in the conversation.
|
|
498
|
+
for (const seq of surfaceNodes) {
|
|
499
|
+
const event = eventBySeq.get(seq)
|
|
500
|
+
if (!event || !isHumanPrompt(event)) continue
|
|
501
|
+
if (seq === head) continue
|
|
430
502
|
const { text, attachments } = readMessageText(event)
|
|
431
|
-
const turn = turnOf.get(
|
|
503
|
+
const turn = turnOf.get(seq)
|
|
432
504
|
out.push({
|
|
433
|
-
seq
|
|
505
|
+
seq,
|
|
434
506
|
turn: typeof turn === 'number' ? turn : null,
|
|
435
507
|
messageId: messageIdOf(event) ?? null,
|
|
436
508
|
text,
|
|
@@ -595,20 +667,40 @@ async function flushSession(ctx, session) {
|
|
|
595
667
|
|
|
596
668
|
async function stateOf(ctx, sessionId, config) {
|
|
597
669
|
const events = await readEvents(ctx, sessionId)
|
|
598
|
-
if (!events)
|
|
670
|
+
if (!events) {
|
|
671
|
+
noteRequest({ kind: 'state', sessionId, ok: false, code: 'session-not-found' })
|
|
672
|
+
throw new HttpError(404, 'session-not-found', 'no session log for this id')
|
|
673
|
+
}
|
|
599
674
|
const folded = foldSurface(events)
|
|
600
675
|
const ledger = rollbackLedger(events)
|
|
601
676
|
const live = findLiveSession(ctx, sessionId)
|
|
677
|
+
const turns = editableTurns(events, folded.nodes)
|
|
678
|
+
const replies = editableReplies(events, folded.nodes)
|
|
679
|
+
noteRequest({
|
|
680
|
+
kind: 'state',
|
|
681
|
+
sessionId,
|
|
682
|
+
ok: true,
|
|
683
|
+
events: events.length,
|
|
684
|
+
turns: turns.length,
|
|
685
|
+
replies: replies.length,
|
|
686
|
+
hidden: ledger.hidden.length,
|
|
687
|
+
live: Boolean(live),
|
|
688
|
+
})
|
|
602
689
|
return {
|
|
603
690
|
hidden: ledger.hidden,
|
|
604
691
|
edits: ledger.edits.length,
|
|
692
|
+
// The window record behind every hidden entry, with the same field names the
|
|
693
|
+
// apply response uses (`replacementSeq`, `shadowed`, plus the window bounds).
|
|
694
|
+
// Published so a sibling plugin can follow a rewritten message to its live
|
|
695
|
+
// node without re-deriving the ledger from the log itself.
|
|
696
|
+
revisions: ledger.edits,
|
|
605
697
|
surface: folded.nodes,
|
|
606
|
-
turns
|
|
607
|
-
replies
|
|
698
|
+
turns,
|
|
699
|
+
replies,
|
|
608
700
|
live: Boolean(live),
|
|
609
701
|
busy: isBusy(events),
|
|
610
702
|
lastSeq: events.length > 0 ? events[events.length - 1].seq : -1,
|
|
611
|
-
config: { confirm: config.confirm },
|
|
703
|
+
config: { confirm: config.confirm, rerun: config.rerun === true },
|
|
612
704
|
version: PLUGIN_VERSION,
|
|
613
705
|
}
|
|
614
706
|
}
|
|
@@ -624,17 +716,24 @@ async function stateOf(ctx, sessionId, config) {
|
|
|
624
716
|
export function buildCarrier(plan, lastTurn, config) {
|
|
625
717
|
const silent = config.carrier !== 'user/message'
|
|
626
718
|
if (silent) {
|
|
627
|
-
//
|
|
719
|
+
// The silent carrier is an empty message that projects to no model message.
|
|
720
|
+
// It has to be a `developer/message`: the format admits only system-prompt
|
|
721
|
+
// sources on `system/message`, so an empty plugin-owned system node (what
|
|
722
|
+
// this used to append) made every session that contained one unreadable -
|
|
723
|
+
// `SessionFormatError` on load, after a perfectly accepted append. An empty
|
|
724
|
+
// developer message is legal, dormant, and a replacement never gets a row
|
|
725
|
+
// of its own (rows are built for append events only), so it is invisible in
|
|
726
|
+
// the transcript too.
|
|
628
727
|
return {
|
|
629
|
-
type: '
|
|
728
|
+
type: 'developer/message',
|
|
630
729
|
data: {
|
|
631
730
|
turn: typeof lastTurn === 'number' ? lastTurn : 0,
|
|
632
731
|
step: 1,
|
|
633
732
|
message: {
|
|
634
733
|
id: randomUUID(),
|
|
635
|
-
role: '
|
|
734
|
+
role: 'developer',
|
|
636
735
|
content: [],
|
|
637
|
-
source: { kind: `plugin:${PLUGIN_ID}
|
|
736
|
+
source: { kind: `plugin:${PLUGIN_ID}`, editedBy: PLUGIN_ID },
|
|
638
737
|
},
|
|
639
738
|
},
|
|
640
739
|
}
|
|
@@ -645,7 +744,7 @@ export function buildCarrier(plan, lastTurn, config) {
|
|
|
645
744
|
id: randomUUID(),
|
|
646
745
|
role: 'user',
|
|
647
746
|
content: [{ type: 'text', text: config.markerText }],
|
|
648
|
-
source: { kind: `plugin:${PLUGIN_ID}
|
|
747
|
+
source: { kind: `plugin:${PLUGIN_ID}`, editedBy: PLUGIN_ID },
|
|
649
748
|
},
|
|
650
749
|
}
|
|
651
750
|
}
|
|
@@ -662,19 +761,21 @@ export function buildCarrier(plan, lastTurn, config) {
|
|
|
662
761
|
* ordinary reply, while `editedBy` records honestly that the text was written by
|
|
663
762
|
* the plugin and not produced by the model.
|
|
664
763
|
*
|
|
665
|
-
* @param plan - the planned window, for the original's
|
|
764
|
+
* @param plan - the planned window, for the original's provider/model.
|
|
666
765
|
* @param text - the replacement text.
|
|
766
|
+
* @param turn - the turn the correction lands in (the freshly opened one).
|
|
767
|
+
* @param step - the step the correction lands in.
|
|
667
768
|
* @returns `{ type, data }` for `session.append`, to be marked `surfaceOp: 'append'`.
|
|
668
769
|
*/
|
|
669
|
-
export function buildCorrection(plan, text) {
|
|
770
|
+
export function buildCorrection(plan, text, turn, step) {
|
|
670
771
|
const source = { kind: 'model', editedBy: PLUGIN_ID }
|
|
671
772
|
if (typeof plan.provider === 'string' && plan.provider !== '') source.provider = plan.provider
|
|
672
773
|
if (typeof plan.model === 'string' && plan.model !== '') source.model = plan.model
|
|
673
774
|
return {
|
|
674
775
|
type: 'assistant/message',
|
|
675
776
|
data: {
|
|
676
|
-
turn: typeof
|
|
677
|
-
step: typeof
|
|
777
|
+
turn: typeof turn === 'number' ? turn : 0,
|
|
778
|
+
step: typeof step === 'number' ? step : 1,
|
|
678
779
|
message: {
|
|
679
780
|
id: randomUUID(),
|
|
680
781
|
role: 'assistant',
|
|
@@ -721,14 +822,40 @@ async function applyEdit(ctx, sessionId, body, config) {
|
|
|
721
822
|
if (!surfaceNodes.includes(seq)) throw new HttpError(409, 'stale', 'the session changed, retry')
|
|
722
823
|
}
|
|
723
824
|
|
|
724
|
-
|
|
825
|
+
// A revised prompt has to (a) replace the old wording in the model context and
|
|
826
|
+
// (b) NOT be answered. The platform answers any user message it finds on the
|
|
827
|
+
// surface, so the revision cannot be *appended* - appending one made the loop
|
|
828
|
+
// answer it, and the next save added another copy (three "你好" bubbles). It
|
|
829
|
+
// travels as the rollback's replacement instead: a replace-type carrier, which
|
|
830
|
+
// removes the old prompt and stands in its place, in one event.
|
|
831
|
+
// A reply edit needs somewhere to land: `assistant/message` (and any message
|
|
832
|
+
// with a step coordinate) is only readable inside an OPEN turn and step, and
|
|
833
|
+
// opening one in the log is what advances the runtime's own turn counter too
|
|
834
|
+
// (it projects `lastTurn` from every turn/start it observes). A prompt edit
|
|
835
|
+
// does not: `user/message` carries the revision and needs no turn.
|
|
836
|
+
const editTurn = plan.mode === 'reply' ? nextTurnOf(events) : lastTurnOf(events)
|
|
837
|
+
const carrier = buildCarrier(plan, editTurn, plan.mode === 'prompt' ? { ...config, carrier: 'user/message', markerText: text } : config)
|
|
838
|
+
if (plan.mode === 'reply') {
|
|
839
|
+
try {
|
|
840
|
+
session.append('turn/start', { turn: editTurn })
|
|
841
|
+
session.append('step/start', { turn: editTurn, step: 1 })
|
|
842
|
+
} catch (error) {
|
|
843
|
+
return {
|
|
844
|
+
kind: 'reply',
|
|
845
|
+
applied: false,
|
|
846
|
+
applyError: `could not open a turn for the correction: ${String((error && error.message) || error)}`,
|
|
847
|
+
shadowed: plan.shadowed,
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
725
851
|
let replacement
|
|
726
852
|
try {
|
|
727
853
|
// A replacement carrier has to be a surface event with non-empty
|
|
728
854
|
// `sourceEventSeqs` coverage of the whole shadowed window. An assistant
|
|
729
855
|
// message forbids `sourceEventSeqs` outright, so the carrier is either an
|
|
730
|
-
// empty `
|
|
731
|
-
//
|
|
856
|
+
// empty `developer/message` (default: dormant, invisible to the model, and
|
|
857
|
+
// never given a row of its own because rows are built for appends) or a
|
|
858
|
+
// short `user/message` marker.
|
|
732
859
|
replacement = session.append(carrier.type, carrier.data, {
|
|
733
860
|
surfaceOp: { op: 'replace', startSeq: plan.startSeq, endSeq: plan.endSeq },
|
|
734
861
|
sourceEventSeqs: plan.shadowed,
|
|
@@ -748,10 +875,15 @@ async function applyEdit(ctx, sessionId, body, config) {
|
|
|
748
875
|
// continues from there. Nothing is re-run: the answer was fixed, the model was
|
|
749
876
|
// not asked again.
|
|
750
877
|
if (plan.mode === 'reply') {
|
|
751
|
-
const correction = buildCorrection(plan, text)
|
|
878
|
+
const correction = buildCorrection(plan, text, editTurn, 1)
|
|
752
879
|
let appended
|
|
753
880
|
try {
|
|
754
881
|
appended = session.append(correction.type, correction.data, { surfaceOp: 'append' })
|
|
882
|
+
// Close the step and the turn behind the corrected reply: the host anchors
|
|
883
|
+
// a turn's tail (duration, action strip) at its last `turn/end`, and the
|
|
884
|
+
// format only reads a step message inside an open turn and step.
|
|
885
|
+
session.append('step/end', { turn: editTurn, step: 1 })
|
|
886
|
+
session.append('turn/end', { turn: editTurn, reason: { kind: 'completed' } })
|
|
755
887
|
} catch (error) {
|
|
756
888
|
return {
|
|
757
889
|
kind: 'reply',
|
|
@@ -773,15 +905,31 @@ async function applyEdit(ctx, sessionId, body, config) {
|
|
|
773
905
|
}
|
|
774
906
|
}
|
|
775
907
|
|
|
776
|
-
//
|
|
777
|
-
//
|
|
908
|
+
// The rollback above already did the whole job: the carrier that replaced the
|
|
909
|
+
// window *is* the revised prompt, so the model reads it as the user's wording
|
|
910
|
+
// from here on and nothing has to be prompted. Saving therefore costs no model
|
|
911
|
+
// call - which is the point. `rerun: true` restores the older behaviour of
|
|
912
|
+
// asking the model right away (through the one official prompt path, which also
|
|
913
|
+
// resumes a cold Session and starts exactly one new turn).
|
|
914
|
+
if (config.rerun !== true) {
|
|
915
|
+
return {
|
|
916
|
+
kind: 'prompt',
|
|
917
|
+
replacementSeq: replacement.seq,
|
|
918
|
+
applied: true,
|
|
919
|
+
reran: false,
|
|
920
|
+
shadowed: plan.shadowed,
|
|
921
|
+
...flush,
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
778
925
|
const controller = ctx.get('sessionController')
|
|
779
926
|
if (!controller || typeof controller.prompt !== 'function') {
|
|
780
927
|
return {
|
|
781
928
|
kind: 'prompt',
|
|
782
929
|
replacementSeq: replacement.seq,
|
|
783
|
-
|
|
784
|
-
|
|
930
|
+
applied: true,
|
|
931
|
+
reran: false,
|
|
932
|
+
rerunError: 'sessionController.prompt unavailable',
|
|
785
933
|
shadowed: plan.shadowed,
|
|
786
934
|
...flush,
|
|
787
935
|
}
|
|
@@ -800,8 +948,9 @@ async function applyEdit(ctx, sessionId, body, config) {
|
|
|
800
948
|
return {
|
|
801
949
|
kind: 'prompt',
|
|
802
950
|
replacementSeq: replacement.seq,
|
|
803
|
-
|
|
804
|
-
|
|
951
|
+
applied: true,
|
|
952
|
+
reran: false,
|
|
953
|
+
rerunError: String((error && error.message) || error),
|
|
805
954
|
shadowed: plan.shadowed,
|
|
806
955
|
...flush,
|
|
807
956
|
}
|
|
@@ -810,7 +959,8 @@ async function applyEdit(ctx, sessionId, body, config) {
|
|
|
810
959
|
return {
|
|
811
960
|
kind: 'prompt',
|
|
812
961
|
replacementSeq: replacement.seq,
|
|
813
|
-
|
|
962
|
+
applied: true,
|
|
963
|
+
reran: true,
|
|
814
964
|
shadowed: plan.shadowed,
|
|
815
965
|
...flush,
|
|
816
966
|
}
|
|
@@ -990,6 +1140,7 @@ export function apply(ctx, config) {
|
|
|
990
1140
|
: DEFAULT_MARKER,
|
|
991
1141
|
carrier: config && config.carrier === 'user/message' ? 'user/message' : 'system/message',
|
|
992
1142
|
confirm: config && typeof config.confirm === 'boolean' ? config.confirm : false,
|
|
1143
|
+
rerun: config && config.rerun === true,
|
|
993
1144
|
}
|
|
994
1145
|
|
|
995
1146
|
const registerRoutes = (webServer, fiber) => {
|
|
@@ -1013,6 +1164,24 @@ export function apply(ctx, config) {
|
|
|
1013
1164
|
}),
|
|
1014
1165
|
)
|
|
1015
1166
|
|
|
1167
|
+
// Read-only diagnostics. The browser half's own state cannot be observed
|
|
1168
|
+
// from here, so the next best thing is an honest record of what its requests
|
|
1169
|
+
// asked for and what they got back.
|
|
1170
|
+
fiber.effect(() =>
|
|
1171
|
+
webServer.register({
|
|
1172
|
+
kind: 'exact',
|
|
1173
|
+
path: `${ROUTE_PREFIX}/debug`,
|
|
1174
|
+
handler: (req, res) => {
|
|
1175
|
+
if (!guard(req, res)) return
|
|
1176
|
+
if (req.method !== 'GET') {
|
|
1177
|
+
sendJson(res, 405, { ok: false, code: 'method', error: 'GET only' })
|
|
1178
|
+
return
|
|
1179
|
+
}
|
|
1180
|
+
sendJson(res, 200, { ok: true, version: PLUGIN_VERSION, requests: recentRequests() })
|
|
1181
|
+
},
|
|
1182
|
+
}),
|
|
1183
|
+
)
|
|
1184
|
+
|
|
1016
1185
|
fiber.effect(() =>
|
|
1017
1186
|
webServer.register({
|
|
1018
1187
|
kind: 'exact',
|
|
@@ -1033,8 +1202,26 @@ export function apply(ctx, config) {
|
|
|
1033
1202
|
}
|
|
1034
1203
|
try {
|
|
1035
1204
|
const sessionId = requireSessionId(typeof body.sessionId === 'string' ? body.sessionId.trim() : '')
|
|
1036
|
-
|
|
1205
|
+
const result = await applyEdit(ctx, sessionId, body, resolved)
|
|
1206
|
+
// Recorded so the next "the button does nothing" can be answered
|
|
1207
|
+
// from here: an apply that never appears was never sent - the click
|
|
1208
|
+
// was lost in the browser half, not refused by this one.
|
|
1209
|
+
noteRequest({
|
|
1210
|
+
kind: 'apply',
|
|
1211
|
+
sessionId,
|
|
1212
|
+
ok: true,
|
|
1213
|
+
edit: result.kind,
|
|
1214
|
+
seq: typeof body.seq === 'number' ? body.seq : null,
|
|
1215
|
+
replacement: typeof result.replacementSeq === 'number' ? result.replacementSeq : null,
|
|
1216
|
+
})
|
|
1217
|
+
sendJson(res, 200, { ok: true, ...result })
|
|
1037
1218
|
} catch (error) {
|
|
1219
|
+
noteRequest({
|
|
1220
|
+
kind: 'apply',
|
|
1221
|
+
sessionId: typeof body.sessionId === 'string' ? body.sessionId.trim() : null,
|
|
1222
|
+
ok: false,
|
|
1223
|
+
code: error && error.code ? String(error.code) : 'internal',
|
|
1224
|
+
})
|
|
1038
1225
|
failure(res, error)
|
|
1039
1226
|
}
|
|
1040
1227
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-edit-turn",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
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",
|
|
@@ -40,11 +40,15 @@
|
|
|
40
40
|
"verify:live": "node tools/verify-live-client.mjs",
|
|
41
41
|
"verify:profile": "bash tools/verify-dsh-plugin.sh",
|
|
42
42
|
"probe:loaded": "node tools/probe-loaded.mjs",
|
|
43
|
-
"check": "npm run test && npm run verify:contract && npm run verify:client"
|
|
43
|
+
"check": "npm run test && npm run verify:contract && npm run verify:client",
|
|
44
|
+
"verify:ui": "node tools/verify-live-ui.mjs"
|
|
44
45
|
},
|
|
45
46
|
"dependencies": {
|
|
46
47
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
47
48
|
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@deepseek-ai/dsh-session": "^0.1.7-rc.2"
|
|
51
|
+
},
|
|
48
52
|
"peerDependencies": {
|
|
49
53
|
"@deepseek-ai/dsh-settings": "^0.1.0-rc.7 || ^0.1.1-rc.2 || ^0.1.2-alpha.2 || ^0.1.3-alpha.2 || ^0.1.5-alpha.1",
|
|
50
54
|
"@deepseek-ai/dsh-tools": "^0.1.0-rc.7 || ^0.1.1-rc.2 || ^0.1.2-alpha.2 || ^0.1.3-alpha.2 || ^0.1.5-alpha.1"
|