dsh-edit-turn 0.2.1 → 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 +413 -62
- package/lib/client.js +625 -128
- package/lib/index.js +194 -64
- 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,7 @@ 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'
|
|
52
64
|
|
|
53
65
|
// A ring of the last few requests, answering "did the browser half even ask, and
|
|
54
66
|
// what did it get?" without needing its console. Read-only diagnostics: the
|
|
@@ -82,16 +94,32 @@ const FLUSH_DEADLINE_MS = 5_000
|
|
|
82
94
|
*/
|
|
83
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.'
|
|
84
96
|
|
|
85
|
-
/**
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
+
})
|
|
95
123
|
|
|
96
124
|
// ---------------------------------------------------------------------------
|
|
97
125
|
// pure session-log logic (no DSH SDK imports; unit-testable under node --test)
|
|
@@ -233,7 +261,13 @@ export function messageIdOf(event) {
|
|
|
233
261
|
export function isHumanPrompt(event) {
|
|
234
262
|
if (!event || event.type !== 'user/message') return false
|
|
235
263
|
const data = event.data
|
|
236
|
-
|
|
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
|
|
237
271
|
}
|
|
238
272
|
|
|
239
273
|
/**
|
|
@@ -313,6 +347,16 @@ export function rollbackLedger(events) {
|
|
|
313
347
|
return { hidden, edits }
|
|
314
348
|
}
|
|
315
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
|
+
|
|
316
360
|
/** Highest turn number recorded in the log, or undefined for an empty log. */
|
|
317
361
|
export function lastTurnOf(events) {
|
|
318
362
|
let last
|
|
@@ -390,14 +434,21 @@ export function planRollback(events, surfaceNodes, request) {
|
|
|
390
434
|
if (target.seq === surfaceNodes[0]) {
|
|
391
435
|
throw new EditPlanError('not-editable', 'the system prompt head cannot be edited')
|
|
392
436
|
}
|
|
393
|
-
// Two editable kinds
|
|
394
|
-
//
|
|
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.
|
|
395
439
|
const mode = isHumanPrompt(target) ? 'prompt' : isAssistantReply(target) ? 'reply' : null
|
|
396
440
|
if (mode === null) {
|
|
397
441
|
throw new EditPlanError('not-editable', 'only your own messages and the model replies can be edited')
|
|
398
442
|
}
|
|
399
443
|
const startIdx = nodeIndex.get(target.seq)
|
|
400
|
-
|
|
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)
|
|
401
452
|
if (shadowed.length === 0 || shadowed[0] !== target.seq) {
|
|
402
453
|
throw new EditPlanError('not-editable', 'the target does not open a rollback window')
|
|
403
454
|
}
|
|
@@ -436,18 +487,22 @@ export function planRollback(events, surfaceNodes, request) {
|
|
|
436
487
|
* @returns one entry per editable prompt, in conversation order.
|
|
437
488
|
*/
|
|
438
489
|
export function editableTurns(events, surfaceNodes) {
|
|
439
|
-
const
|
|
490
|
+
const eventBySeq = new Map(events.map((event) => [event.seq, event]))
|
|
440
491
|
const turnOf = turnIndex(events)
|
|
441
492
|
const head = surfaceNodes[0]
|
|
442
493
|
const out = []
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
|
447
502
|
const { text, attachments } = readMessageText(event)
|
|
448
|
-
const turn = turnOf.get(
|
|
503
|
+
const turn = turnOf.get(seq)
|
|
449
504
|
out.push({
|
|
450
|
-
seq
|
|
505
|
+
seq,
|
|
451
506
|
turn: typeof turn === 'number' ? turn : null,
|
|
452
507
|
messageId: messageIdOf(event) ?? null,
|
|
453
508
|
text,
|
|
@@ -634,13 +689,18 @@ async function stateOf(ctx, sessionId, config) {
|
|
|
634
689
|
return {
|
|
635
690
|
hidden: ledger.hidden,
|
|
636
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,
|
|
637
697
|
surface: folded.nodes,
|
|
638
698
|
turns,
|
|
639
699
|
replies,
|
|
640
700
|
live: Boolean(live),
|
|
641
701
|
busy: isBusy(events),
|
|
642
702
|
lastSeq: events.length > 0 ? events[events.length - 1].seq : -1,
|
|
643
|
-
config: { confirm: config.confirm },
|
|
703
|
+
config: { confirm: config.confirm, rerun: config.rerun === true },
|
|
644
704
|
version: PLUGIN_VERSION,
|
|
645
705
|
}
|
|
646
706
|
}
|
|
@@ -656,17 +716,24 @@ async function stateOf(ctx, sessionId, config) {
|
|
|
656
716
|
export function buildCarrier(plan, lastTurn, config) {
|
|
657
717
|
const silent = config.carrier !== 'user/message'
|
|
658
718
|
if (silent) {
|
|
659
|
-
//
|
|
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.
|
|
660
727
|
return {
|
|
661
|
-
type: '
|
|
728
|
+
type: 'developer/message',
|
|
662
729
|
data: {
|
|
663
730
|
turn: typeof lastTurn === 'number' ? lastTurn : 0,
|
|
664
731
|
step: 1,
|
|
665
732
|
message: {
|
|
666
733
|
id: randomUUID(),
|
|
667
|
-
role: '
|
|
734
|
+
role: 'developer',
|
|
668
735
|
content: [],
|
|
669
|
-
source: { kind: `plugin:${PLUGIN_ID}
|
|
736
|
+
source: { kind: `plugin:${PLUGIN_ID}`, editedBy: PLUGIN_ID },
|
|
670
737
|
},
|
|
671
738
|
},
|
|
672
739
|
}
|
|
@@ -677,7 +744,7 @@ export function buildCarrier(plan, lastTurn, config) {
|
|
|
677
744
|
id: randomUUID(),
|
|
678
745
|
role: 'user',
|
|
679
746
|
content: [{ type: 'text', text: config.markerText }],
|
|
680
|
-
source: { kind: `plugin:${PLUGIN_ID}
|
|
747
|
+
source: { kind: `plugin:${PLUGIN_ID}`, editedBy: PLUGIN_ID },
|
|
681
748
|
},
|
|
682
749
|
}
|
|
683
750
|
}
|
|
@@ -694,19 +761,21 @@ export function buildCarrier(plan, lastTurn, config) {
|
|
|
694
761
|
* ordinary reply, while `editedBy` records honestly that the text was written by
|
|
695
762
|
* the plugin and not produced by the model.
|
|
696
763
|
*
|
|
697
|
-
* @param plan - the planned window, for the original's
|
|
764
|
+
* @param plan - the planned window, for the original's provider/model.
|
|
698
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.
|
|
699
768
|
* @returns `{ type, data }` for `session.append`, to be marked `surfaceOp: 'append'`.
|
|
700
769
|
*/
|
|
701
|
-
export function buildCorrection(plan, text) {
|
|
770
|
+
export function buildCorrection(plan, text, turn, step) {
|
|
702
771
|
const source = { kind: 'model', editedBy: PLUGIN_ID }
|
|
703
772
|
if (typeof plan.provider === 'string' && plan.provider !== '') source.provider = plan.provider
|
|
704
773
|
if (typeof plan.model === 'string' && plan.model !== '') source.model = plan.model
|
|
705
774
|
return {
|
|
706
775
|
type: 'assistant/message',
|
|
707
776
|
data: {
|
|
708
|
-
turn: typeof
|
|
709
|
-
step: typeof
|
|
777
|
+
turn: typeof turn === 'number' ? turn : 0,
|
|
778
|
+
step: typeof step === 'number' ? step : 1,
|
|
710
779
|
message: {
|
|
711
780
|
id: randomUUID(),
|
|
712
781
|
role: 'assistant',
|
|
@@ -753,14 +822,40 @@ async function applyEdit(ctx, sessionId, body, config) {
|
|
|
753
822
|
if (!surfaceNodes.includes(seq)) throw new HttpError(409, 'stale', 'the session changed, retry')
|
|
754
823
|
}
|
|
755
824
|
|
|
756
|
-
|
|
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
|
+
}
|
|
757
851
|
let replacement
|
|
758
852
|
try {
|
|
759
853
|
// A replacement carrier has to be a surface event with non-empty
|
|
760
854
|
// `sourceEventSeqs` coverage of the whole shadowed window. An assistant
|
|
761
855
|
// message forbids `sourceEventSeqs` outright, so the carrier is either an
|
|
762
|
-
// empty `
|
|
763
|
-
//
|
|
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.
|
|
764
859
|
replacement = session.append(carrier.type, carrier.data, {
|
|
765
860
|
surfaceOp: { op: 'replace', startSeq: plan.startSeq, endSeq: plan.endSeq },
|
|
766
861
|
sourceEventSeqs: plan.shadowed,
|
|
@@ -780,17 +875,15 @@ async function applyEdit(ctx, sessionId, body, config) {
|
|
|
780
875
|
// continues from there. Nothing is re-run: the answer was fixed, the model was
|
|
781
876
|
// not asked again.
|
|
782
877
|
if (plan.mode === 'reply') {
|
|
783
|
-
const correction = buildCorrection(plan, text)
|
|
878
|
+
const correction = buildCorrection(plan, text, editTurn, 1)
|
|
784
879
|
let appended
|
|
785
880
|
try {
|
|
786
881
|
appended = session.append(correction.type, correction.data, { surfaceOp: 'append' })
|
|
787
|
-
// Close the
|
|
788
|
-
//
|
|
789
|
-
//
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
session.append('step/end', { turn: appended.data.turn, step: appended.data.step })
|
|
793
|
-
session.append('turn/end', { turn: appended.data.turn, reason: { kind: 'completed' } })
|
|
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' } })
|
|
794
887
|
} catch (error) {
|
|
795
888
|
return {
|
|
796
889
|
kind: 'reply',
|
|
@@ -812,15 +905,31 @@ async function applyEdit(ctx, sessionId, body, config) {
|
|
|
812
905
|
}
|
|
813
906
|
}
|
|
814
907
|
|
|
815
|
-
//
|
|
816
|
-
//
|
|
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
|
+
|
|
817
925
|
const controller = ctx.get('sessionController')
|
|
818
926
|
if (!controller || typeof controller.prompt !== 'function') {
|
|
819
927
|
return {
|
|
820
928
|
kind: 'prompt',
|
|
821
929
|
replacementSeq: replacement.seq,
|
|
822
|
-
|
|
823
|
-
|
|
930
|
+
applied: true,
|
|
931
|
+
reran: false,
|
|
932
|
+
rerunError: 'sessionController.prompt unavailable',
|
|
824
933
|
shadowed: plan.shadowed,
|
|
825
934
|
...flush,
|
|
826
935
|
}
|
|
@@ -839,8 +948,9 @@ async function applyEdit(ctx, sessionId, body, config) {
|
|
|
839
948
|
return {
|
|
840
949
|
kind: 'prompt',
|
|
841
950
|
replacementSeq: replacement.seq,
|
|
842
|
-
|
|
843
|
-
|
|
951
|
+
applied: true,
|
|
952
|
+
reran: false,
|
|
953
|
+
rerunError: String((error && error.message) || error),
|
|
844
954
|
shadowed: plan.shadowed,
|
|
845
955
|
...flush,
|
|
846
956
|
}
|
|
@@ -849,7 +959,8 @@ async function applyEdit(ctx, sessionId, body, config) {
|
|
|
849
959
|
return {
|
|
850
960
|
kind: 'prompt',
|
|
851
961
|
replacementSeq: replacement.seq,
|
|
852
|
-
|
|
962
|
+
applied: true,
|
|
963
|
+
reran: true,
|
|
853
964
|
shadowed: plan.shadowed,
|
|
854
965
|
...flush,
|
|
855
966
|
}
|
|
@@ -1029,6 +1140,7 @@ export function apply(ctx, config) {
|
|
|
1029
1140
|
: DEFAULT_MARKER,
|
|
1030
1141
|
carrier: config && config.carrier === 'user/message' ? 'user/message' : 'system/message',
|
|
1031
1142
|
confirm: config && typeof config.confirm === 'boolean' ? config.confirm : false,
|
|
1143
|
+
rerun: config && config.rerun === true,
|
|
1032
1144
|
}
|
|
1033
1145
|
|
|
1034
1146
|
const registerRoutes = (webServer, fiber) => {
|
|
@@ -1090,8 +1202,26 @@ export function apply(ctx, config) {
|
|
|
1090
1202
|
}
|
|
1091
1203
|
try {
|
|
1092
1204
|
const sessionId = requireSessionId(typeof body.sessionId === 'string' ? body.sessionId.trim() : '')
|
|
1093
|
-
|
|
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 })
|
|
1094
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
|
+
})
|
|
1095
1225
|
failure(res, error)
|
|
1096
1226
|
}
|
|
1097
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"
|