@khorsheed/dsh-client-message-tools 0.1.0

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.
Files changed (72) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +21 -0
  3. package/README.en.md +82 -0
  4. package/README.i18n.yaml +6 -0
  5. package/README.md +82 -0
  6. package/cordis.patch.yml +6 -0
  7. package/lib/client.js +5824 -0
  8. package/lib/client.js.map +1 -0
  9. package/lib/index.js +644 -0
  10. package/lib/invariant.js +27 -0
  11. package/lib/tsconfig.client.tsbuildinfo +1 -0
  12. package/lib/tsconfig.host.tsbuildinfo +1 -0
  13. package/lib/typert.host.d.ts +3 -0
  14. package/lib/typert.host.js +250 -0
  15. package/lib/typert.remote-client.d.ts +26 -0
  16. package/lib/typert.remote-client.d.ts.map +1 -0
  17. package/lib/typert.remote-client.js +154 -0
  18. package/lib/types/client/ModelChip.d.ts +29 -0
  19. package/lib/types/client/ModelChip.d.ts.map +1 -0
  20. package/lib/types/client/ModelChip.js +86 -0
  21. package/lib/types/client/RestoredMessageView.d.ts +17 -0
  22. package/lib/types/client/RestoredMessageView.d.ts.map +1 -0
  23. package/lib/types/client/RestoredMessageView.js +9 -0
  24. package/lib/types/client/UserMessageView.d.ts +16 -0
  25. package/lib/types/client/UserMessageView.d.ts.map +1 -0
  26. package/lib/types/client/UserMessageView.js +172 -0
  27. package/lib/types/client/WithdrawnDividerView.d.ts +17 -0
  28. package/lib/types/client/WithdrawnDividerView.d.ts.map +1 -0
  29. package/lib/types/client/WithdrawnDividerView.js +59 -0
  30. package/lib/types/client/backfill.d.ts +19 -0
  31. package/lib/types/client/backfill.d.ts.map +1 -0
  32. package/lib/types/client/backfill.js +20 -0
  33. package/lib/types/client/dom-hider.d.ts +61 -0
  34. package/lib/types/client/dom-hider.d.ts.map +1 -0
  35. package/lib/types/client/dom-hider.js +195 -0
  36. package/lib/types/client/edit-in-place.d.ts +107 -0
  37. package/lib/types/client/edit-in-place.d.ts.map +1 -0
  38. package/lib/types/client/edit-in-place.js +97 -0
  39. package/lib/types/client/icons.d.ts +23 -0
  40. package/lib/types/client/icons.d.ts.map +1 -0
  41. package/lib/types/client/icons.js +11 -0
  42. package/lib/types/client/index.d.ts +39 -0
  43. package/lib/types/client/index.d.ts.map +1 -0
  44. package/lib/types/client/index.js +187 -0
  45. package/lib/types/client/locales.d.ts +74 -0
  46. package/lib/types/client/locales.d.ts.map +1 -0
  47. package/lib/types/client/locales.js +65 -0
  48. package/lib/types/client/slots.d.ts +44 -0
  49. package/lib/types/client/slots.d.ts.map +1 -0
  50. package/lib/types/client/slots.js +1 -0
  51. package/lib/types/client/withdraw-backfill.d.ts +25 -0
  52. package/lib/types/client/withdraw-backfill.d.ts.map +1 -0
  53. package/lib/types/client/withdraw-backfill.js +21 -0
  54. package/lib/types/client/withdrawn-node.d.ts +163 -0
  55. package/lib/types/client/withdrawn-node.d.ts.map +1 -0
  56. package/lib/types/client/withdrawn-node.js +297 -0
  57. package/lib/types/index.d.ts +69 -0
  58. package/lib/types/index.d.ts.map +1 -0
  59. package/lib/types/index.js +175 -0
  60. package/lib/types/invariant.d.ts +16 -0
  61. package/lib/types/invariant.d.ts.map +1 -0
  62. package/lib/types/invariant.js +25 -0
  63. package/lib/types/marker.d.ts +94 -0
  64. package/lib/types/marker.d.ts.map +1 -0
  65. package/lib/types/marker.js +131 -0
  66. package/lib/types/types.d.ts +116 -0
  67. package/lib/types/types.d.ts.map +1 -0
  68. package/lib/types/types.js +1 -0
  69. package/lib/types/withdraw.d.ts +112 -0
  70. package/lib/types/withdraw.d.ts.map +1 -0
  71. package/lib/types/withdraw.js +279 -0
  72. package/package.json +108 -0
package/lib/index.js ADDED
@@ -0,0 +1,644 @@
1
+ import { createUserMessage } from "@deepseek-ai/dsh-llm";
2
+ import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
3
+ import { isAppendSurfaceEvent, isReplacementSurfaceEvent } from "@deepseek-ai/dsh-session/surface";
4
+ //#region lib/types/marker.js
5
+ /**
6
+ * Shared message-tools markers: the plugin source tag stamped on withdrawal
7
+ * replacement events, the `op` discriminator telling edit replacements, edit
8
+ * triggers, and assistant-text restore replays apart from withdrawals and
9
+ * user-message restores, the model-facing placeholder and trigger texts, and
10
+ * the pure predicates both the Host service and the browser projection
11
+ * consume. No new session event type is ever written — an
12
+ * out-of-harness event type cannot carry `ignorable: true` (Session.append
13
+ * assigns the envelope), so a persisted unknown type would make
14
+ * session-persistence refuse the log on reload. The events themselves are the
15
+ * durable audit trail.
16
+ * @module @khorsheed/dsh-client-message-tools/marker
17
+ */
18
+ /** Plugin source tag identifying every message-tools event. */
19
+ const MESSAGE_TOOLS_PLUGIN = "message-tools";
20
+ /**
21
+ * Model-facing placeholder left on the surface in place of the withdrawn
22
+ * span. The span itself is hidden from the model; this minimal user message
23
+ * keeps the surface non-empty and tells the model why the transcript jumps.
24
+ */
25
+ const WITHDRAWN_NOTICE = "(用户撤回了这条消息及其后的所有内容)";
26
+ /**
27
+ * Model-facing trigger message that starts the regeneration turn after an
28
+ * edit replacement lands (no wake-without-append path exists in the harness;
29
+ * `agent.followup` is the sanctioned seam). It is NOT the edited text — that
30
+ * appears exactly once, in the replacement event.
31
+ */
32
+ const EDIT_TRIGGER_NOTICE = "(用户编辑了上一条消息,请按编辑后的内容重新回答)";
33
+ /**
34
+ * Model-facing frame preceding a replayed assistant text in a restore: the
35
+ * replay lands as a user-role message (assistant/message cannot carry a
36
+ * plugin source, and the session turn/step trace forbids ad-hoc assistant
37
+ * appends), so the frame keeps the role honest for the model.
38
+ */
39
+ const RESTORED_ASSISTANT_NOTICE = "(以下是先前被撤回、现随恢复放回的助手回复)";
40
+ /** The plugin-tagged source of an edit replacement (op: 'edit'). */
41
+ function editReplacementSource() {
42
+ return Object.freeze({
43
+ kind: "plugin",
44
+ plugin: MESSAGE_TOOLS_PLUGIN,
45
+ op: "edit"
46
+ });
47
+ }
48
+ /** The plugin-tagged source of an edit trigger message (op: 'edit-trigger'). */
49
+ function editTriggerSource() {
50
+ return Object.freeze({
51
+ kind: "plugin",
52
+ plugin: MESSAGE_TOOLS_PLUGIN,
53
+ op: "edit-trigger"
54
+ });
55
+ }
56
+ /** The plugin-tagged source of a restored assistant-text replay (op: 'restore-assistant'). */
57
+ function restoreAssistantSource() {
58
+ return Object.freeze({
59
+ kind: "plugin",
60
+ plugin: MESSAGE_TOOLS_PLUGIN,
61
+ op: "restore-assistant"
62
+ });
63
+ }
64
+ /** Read the op discriminator off a message source (absent on legacy events). */
65
+ function messageToolsOp(source) {
66
+ const op = source?.op;
67
+ return op === "edit" || op === "edit-trigger" || op === "restore-assistant" ? op : void 0;
68
+ }
69
+ /**
70
+ * Whether the event is a message-tools WITHDRAWAL replacement: a
71
+ * `user/message` surface replacement whose source carries this plugin's tag
72
+ * and no `op` discriminator (legacy and current withdrawals both).
73
+ * @param event - the session event to test.
74
+ * @returns the event narrowed to a withdrawal replacement when matched.
75
+ */
76
+ function isMessageToolsReplacement(event) {
77
+ return event.type === "user/message" && isReplacementSurfaceEvent(event) && event.data.source.kind === "plugin" && event.data.source.plugin === "message-tools" && messageToolsOp(event.data.source) === void 0;
78
+ }
79
+ /**
80
+ * Whether the event is a message-tools EDIT replacement: a `user/message`
81
+ * surface replacement whose content IS the edited text (`op: 'edit'`).
82
+ * @param event - the session event to test.
83
+ * @returns the event narrowed to an edit replacement when matched.
84
+ */
85
+ function isMessageToolsEdit(event) {
86
+ return event.type === "user/message" && isReplacementSurfaceEvent(event) && event.data.source.kind === "plugin" && event.data.source.plugin === "message-tools" && messageToolsOp(event.data.source) === "edit";
87
+ }
88
+ /**
89
+ * Whether the event is a message-tools restore: an append-surface
90
+ * `user/message` replaying a withdrawn message at the conversation tail.
91
+ * @param event - the session event to test.
92
+ * @returns the event narrowed to a message-tools restore when matched.
93
+ */
94
+ function isMessageToolsRestore(event) {
95
+ return event.type === "user/message" && event.surfaceOp === "append" && event.data.source.kind === "plugin" && event.data.source.plugin === "message-tools" && messageToolsOp(event.data.source) === void 0;
96
+ }
97
+ /**
98
+ * Whether the event is a message-tools ASSISTANT-text restore replay: an
99
+ * append-surface `user/message` carrying a withdrawn assistant reply's text
100
+ * (framed, user-role — see {@link RESTORED_ASSISTANT_NOTICE}).
101
+ * @param event - the session event to test.
102
+ * @returns the event narrowed to a restore-assistant replay when matched.
103
+ */
104
+ function isMessageToolsRestoreAssistant(event) {
105
+ return event.type === "user/message" && event.surfaceOp === "append" && event.data.source.kind === "plugin" && event.data.source.plugin === "message-tools" && messageToolsOp(event.data.source) === "restore-assistant";
106
+ }
107
+ //#endregion
108
+ //#region lib/types/withdraw.js
109
+ /**
110
+ * Withdrawal planning: a pure fold from the session log and the live surface
111
+ * to the replacement intent. Kept free of cordis and Session internals so the
112
+ * unit tests drive it directly; the service only validates and appends.
113
+ * @module @khorsheed/dsh-client-message-tools/withdraw
114
+ */
115
+ /**
116
+ * Whether a target seq names an editable user message: an append-surface
117
+ * user-source message (an original), a message-tools restore replay (a
118
+ * restored row withdraws and edits like the message it replays), or a
119
+ * message-tools edit replacement (edit chains — the previous edit's
120
+ * replacement is the surface node). Assistant-text restore replays carry no
121
+ * actions and are excluded.
122
+ */
123
+ function isEditableTarget(event) {
124
+ if (event === void 0 || event.type !== "user/message") return false;
125
+ if (isAppendSurfaceEvent(event)) return event.data.source.kind === "user" || isMessageToolsRestore(event);
126
+ return isMessageToolsEdit(event);
127
+ }
128
+ /** The span [target, surface tail] with full provenance, or undefined when off-surface. */
129
+ function surfaceSpan(surfaceNodes, targetSeq) {
130
+ const startIndex = surfaceNodes.indexOf(targetSeq);
131
+ if (startIndex === -1) return void 0;
132
+ const sourceEventSeqs = surfaceNodes.slice(startIndex);
133
+ const end = sourceEventSeqs[sourceEventSeqs.length - 1];
134
+ /* v8 ignore next -- the slice is non-empty by construction (startIndex names a member) */
135
+ if (end === void 0) return void 0;
136
+ return {
137
+ start: targetSeq,
138
+ end,
139
+ sourceEventSeqs
140
+ };
141
+ }
142
+ /**
143
+ * Plan a withdrawal: the target must be an editable user message (original,
144
+ * restore replay, or edit replacement — see `isEditableTarget`) still present
145
+ * on the live surface; the span covers it and every surface node after it.
146
+ * @param events - the full session log (seq === index).
147
+ * @param surfaceNodes - the current model-visible surface seqs, in order.
148
+ * @param targetSeq - seq of the user message to withdraw.
149
+ * @returns the replacement intent, or the failure code.
150
+ */
151
+ function planWithdrawal(events, surfaceNodes, targetSeq) {
152
+ const target = events[targetSeq];
153
+ if (target === void 0) return {
154
+ ok: false,
155
+ code: "target-not-found"
156
+ };
157
+ if (!isEditableTarget(target)) return {
158
+ ok: false,
159
+ code: "not-a-user-message"
160
+ };
161
+ const span = surfaceSpan(surfaceNodes, targetSeq);
162
+ if (span === void 0) return {
163
+ ok: false,
164
+ code: "already-withdrawn"
165
+ };
166
+ return {
167
+ ok: true,
168
+ plan: span
169
+ };
170
+ }
171
+ /**
172
+ * Plan an edit: the target must be an editable user message still present on
173
+ * the live surface, and the new text must be non-blank. The span covers the
174
+ * target and every surface node after it — editing an old message discards
175
+ * what followed it. The replacement's content is the new text (no
176
+ * placeholder), so the model context reads the edited message in place.
177
+ * @param events - the full session log (seq === index).
178
+ * @param surfaceNodes - the current model-visible surface seqs, in order.
179
+ * @param targetSeq - seq of the user message to edit.
180
+ * @param text - the edited text.
181
+ * @returns the replacement intent, or the failure code.
182
+ */
183
+ function planEdit(events, surfaceNodes, targetSeq, text) {
184
+ if (text.trim() === "") return {
185
+ ok: false,
186
+ code: "empty-text"
187
+ };
188
+ const target = events[targetSeq];
189
+ if (target === void 0) return {
190
+ ok: false,
191
+ code: "target-not-found"
192
+ };
193
+ if (!isEditableTarget(target)) return {
194
+ ok: false,
195
+ code: "not-a-user-message"
196
+ };
197
+ const span = surfaceSpan(surfaceNodes, targetSeq);
198
+ if (span === void 0) return {
199
+ ok: false,
200
+ code: "already-withdrawn"
201
+ };
202
+ return {
203
+ ok: true,
204
+ plan: span
205
+ };
206
+ }
207
+ /** Join the text blocks of one message's content into one plain string. */
208
+ function joinText(blocks) {
209
+ return blocks.filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join("");
210
+ }
211
+ /**
212
+ * Join an assistant message's visible text. Normal replies use text blocks;
213
+ * interrupted replies may contain only reasoning. Prefer text when present,
214
+ * otherwise preserve the reasoning so a restore does not drop the only
215
+ * assistant content the user saw.
216
+ */
217
+ function joinAssistantMessageText(blocks) {
218
+ const text = joinText(blocks);
219
+ if (text !== "") return text;
220
+ return blocks.filter((block) => block.type === "reasoning" && typeof block.text === "string").map((block) => block.text).join("");
221
+ }
222
+ /**
223
+ * The full log seqs shadowed by the latest message-tools withdrawal
224
+ * replacement citing `targetSeq`, or undefined when no such replacement exists
225
+ * (the message left the surface through another producer, e.g. compaction).
226
+ *
227
+ * `sourceEventSeqs` is authoritative for the shadowed *surface* nodes, but an
228
+ * interrupted assistant step may exist only as `assistant/chunk` events (no
229
+ * `assistant/message` ever landed). Those chunks are not surface nodes, so
230
+ * they are absent from `sourceEventSeqs`. The replacement's own `start` and
231
+ * `seq` delimit the whole withdrawn log interval — `[start, seq)` — and are
232
+ * still read from the replacement itself, never guessed from another producer.
233
+ */
234
+ function findWithdrawnSpan(events, targetSeq) {
235
+ for (let index = events.length - 1; index > targetSeq; index--) {
236
+ const event = events[index];
237
+ if (event !== void 0 && isMessageToolsReplacement(event) && event.sourceEventSeqs?.includes(targetSeq)) {
238
+ const span = [];
239
+ for (let seq = event.surfaceOp.start; seq < event.seq; seq++) span.push(seq);
240
+ return span;
241
+ }
242
+ }
243
+ }
244
+ /**
245
+ * Fold one withdrawn span into its replay entries, in span (original) order.
246
+ * Replayed: user-source messages, edit replacements (their content is the
247
+ * last edit's new text), earlier restore replays, and assistant text (framed;
248
+ * restore-assistant replays already carry the frame). Skipped: withdrawal
249
+ * placeholders, edit triggers, other plugin context, and tool calls/results.
250
+ *
251
+ * Assistant text normally comes from `assistant/message` surface events. A
252
+ * turn that was interrupted before finalization may never emit
253
+ * `assistant/message`; its visible content survives only as `assistant/chunk`
254
+ * rows. Those chunks are folded into one assistant replay per step when no
255
+ * final `assistant/message` exists for that step, so withdrawing/restoring an
256
+ * interrupted reply does not lose the partial assistant content.
257
+ */
258
+ function replayEntries(events, spanSeqs) {
259
+ const entries = [];
260
+ let pending;
261
+ const finalizedSteps = /* @__PURE__ */ new Set();
262
+ const flushPending = () => {
263
+ if (pending === void 0) return;
264
+ if (!pending.finalized) {
265
+ const text = [...pending.text.values()].join("");
266
+ const reasoning = [...pending.reasoning.values()].join("");
267
+ const replay = text !== "" ? text : reasoning;
268
+ if (replay !== "") entries.push({
269
+ role: "assistant",
270
+ text: `${RESTORED_ASSISTANT_NOTICE}\n${replay}`,
271
+ sourceSeq: pending.sourceSeq
272
+ });
273
+ }
274
+ pending = void 0;
275
+ };
276
+ const pushChunk = (event) => {
277
+ if (finalizedSteps.has(`${event.data.turn}:${event.data.step}`)) return;
278
+ if (pending === void 0 || pending.turn !== event.data.turn || pending.step !== event.data.step) {
279
+ flushPending();
280
+ pending = {
281
+ turn: event.data.turn,
282
+ step: event.data.step,
283
+ sourceSeq: event.seq,
284
+ text: /* @__PURE__ */ new Map(),
285
+ reasoning: /* @__PURE__ */ new Map(),
286
+ finalized: false
287
+ };
288
+ }
289
+ const chunk = event.data.chunk;
290
+ if (chunk.type === "text-delta") pending.text.set(chunk.index, (pending.text.get(chunk.index) ?? "") + chunk.text);
291
+ else if (chunk.type === "block-end" && chunk.block.type === "text") pending.text.set(chunk.index, chunk.block.text);
292
+ else if (chunk.type === "reasoning-delta") pending.reasoning.set(chunk.index, (pending.reasoning.get(chunk.index) ?? "") + chunk.text);
293
+ else if (chunk.type === "block-end" && chunk.block.type === "reasoning") pending.reasoning.set(chunk.index, chunk.block.text);
294
+ };
295
+ const pushAssistantText = (text, sourceSeq) => {
296
+ if (text !== "") entries.push({
297
+ role: "assistant",
298
+ text: `${RESTORED_ASSISTANT_NOTICE}\n${text}`,
299
+ sourceSeq
300
+ });
301
+ };
302
+ for (const seq of spanSeqs) {
303
+ const event = events[seq];
304
+ if (event === void 0) continue;
305
+ if (isMessageToolsEdit(event)) {
306
+ flushPending();
307
+ entries.push({
308
+ role: "user",
309
+ content: event.data.content,
310
+ sourceSeq: seq
311
+ });
312
+ continue;
313
+ }
314
+ if (event.type === "assistant/chunk") {
315
+ pushChunk(event);
316
+ continue;
317
+ }
318
+ if (event.type === "user/message") {
319
+ flushPending();
320
+ if (event.surfaceOp !== "append") continue;
321
+ if (event.data.source.kind === "user" || isMessageToolsRestore(event)) entries.push({
322
+ role: "user",
323
+ content: event.data.content,
324
+ sourceSeq: seq
325
+ });
326
+ else if (isMessageToolsRestoreAssistant(event)) entries.push({
327
+ role: "assistant",
328
+ text: joinText(event.data.content),
329
+ sourceSeq: seq
330
+ });
331
+ continue;
332
+ }
333
+ if (event.type === "assistant/message") {
334
+ finalizedSteps.add(`${event.data.turn}:${event.data.step}`);
335
+ if (pending !== void 0 && pending.turn === event.data.turn && pending.step === event.data.step) pending.finalized = true;
336
+ flushPending();
337
+ pushAssistantText(joinAssistantMessageText(event.data.message.content), seq);
338
+ continue;
339
+ }
340
+ }
341
+ flushPending();
342
+ return entries;
343
+ }
344
+ /**
345
+ * Plan a restore: the target must be an editable user message (the same
346
+ * vocabulary as withdrawal — original, restore replay, or edit replacement)
347
+ * that is no longer on the live surface (i.e. withdrawn). Restoring an edit
348
+ * replacement replays the span's last edit text — the replacement's content
349
+ * IS the new text. The surface model is positional — a replaced span folds
350
+ * into exactly one node — so the withdrawn span can never be put back in
351
+ * place; restore replays the span's replayable content (user messages, the
352
+ * last edit's new text, assistant text) as fresh tail messages in original
353
+ * order. When no message-tools replacement cites the target (a foreign
354
+ * shadowing, e.g. compaction), only the target message replays — the span
355
+ * boundary is never re-guessed.
356
+ * @param events - the full session log (seq === index).
357
+ * @param surfaceNodes - the current model-visible surface seqs.
358
+ * @param targetSeq - seq of the withdrawn user message to restore.
359
+ * @returns the restore intent, or the failure code.
360
+ */
361
+ function planRestore(events, surfaceNodes, targetSeq) {
362
+ const target = events[targetSeq];
363
+ if (target === void 0) return {
364
+ ok: false,
365
+ code: "target-not-found"
366
+ };
367
+ if (!isEditableTarget(target)) return {
368
+ ok: false,
369
+ code: "not-a-user-message"
370
+ };
371
+ if (surfaceNodes.includes(targetSeq)) return {
372
+ ok: false,
373
+ code: "not-withdrawn"
374
+ };
375
+ const span = findWithdrawnSpan(events, targetSeq);
376
+ return {
377
+ ok: true,
378
+ plan: { entries: span === void 0 ? [{
379
+ role: "user",
380
+ content: target.data.content,
381
+ sourceSeq: targetSeq
382
+ }] : replayEntries(events, span) }
383
+ };
384
+ }
385
+ //#endregion
386
+ //#region lib/types/index.js
387
+ var __runInitializers = function(thisArg, initializers, value) {
388
+ var useValue = arguments.length > 2;
389
+ for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
390
+ return useValue ? value : void 0;
391
+ };
392
+ var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
393
+ function accept(f) {
394
+ if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected");
395
+ return f;
396
+ }
397
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
398
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
399
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
400
+ var _, done = false;
401
+ for (var i = decorators.length - 1; i >= 0; i--) {
402
+ var context = {};
403
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
404
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
405
+ context.addInitializer = function(f) {
406
+ if (done) throw new TypeError("Cannot add initializers after decoration has completed");
407
+ extraInitializers.push(accept(f || null));
408
+ };
409
+ var result = (0, decorators[i])(kind === "accessor" ? {
410
+ get: descriptor.get,
411
+ set: descriptor.set
412
+ } : descriptor[key], context);
413
+ if (kind === "accessor") {
414
+ if (result === void 0) continue;
415
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
416
+ if (_ = accept(result.get)) descriptor.get = _;
417
+ if (_ = accept(result.set)) descriptor.set = _;
418
+ if (_ = accept(result.init)) initializers.unshift(_);
419
+ } else if (_ = accept(result)) {
420
+ if (kind === "field") initializers.unshift(_);
421
+ else descriptor[key] = _;
422
+ }
423
+ }
424
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
425
+ done = true;
426
+ };
427
+ /**
428
+ * messageTools Remote service: appends the withdrawal replacement to the
429
+ * owning Session and flushes it durable. The replacement event's
430
+ * plugin-tagged source is the audit trail; no new session event type is
431
+ * introduced, so session reload stays safe for harnesses without this plugin.
432
+ */
433
+ let MessageToolsService = (() => {
434
+ let _classSuper = TypertRemoteService;
435
+ let _instanceExtraInitializers = [];
436
+ let _withdraw_decorators;
437
+ let _restore_decorators;
438
+ let _edit_decorators;
439
+ return class MessageToolsService extends _classSuper {
440
+ static {
441
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
442
+ _withdraw_decorators = [Remote("withdraw")];
443
+ _restore_decorators = [Remote("restore")];
444
+ _edit_decorators = [Remote("edit")];
445
+ __esDecorate(this, null, _withdraw_decorators, {
446
+ kind: "method",
447
+ name: "withdraw",
448
+ static: false,
449
+ private: false,
450
+ access: {
451
+ has: (obj) => "withdraw" in obj,
452
+ get: (obj) => obj.withdraw
453
+ },
454
+ metadata: _metadata
455
+ }, null, _instanceExtraInitializers);
456
+ __esDecorate(this, null, _restore_decorators, {
457
+ kind: "method",
458
+ name: "restore",
459
+ static: false,
460
+ private: false,
461
+ access: {
462
+ has: (obj) => "restore" in obj,
463
+ get: (obj) => obj.restore
464
+ },
465
+ metadata: _metadata
466
+ }, null, _instanceExtraInitializers);
467
+ __esDecorate(this, null, _edit_decorators, {
468
+ kind: "method",
469
+ name: "edit",
470
+ static: false,
471
+ private: false,
472
+ access: {
473
+ has: (obj) => "edit" in obj,
474
+ get: (obj) => obj.edit
475
+ },
476
+ metadata: _metadata
477
+ }, null, _instanceExtraInitializers);
478
+ if (_metadata) Object.defineProperty(this, Symbol.metadata, {
479
+ enumerable: true,
480
+ configurable: true,
481
+ writable: true,
482
+ value: _metadata
483
+ });
484
+ }
485
+ static inject = ["sessions", "agents"];
486
+ /**
487
+ * @param ctx - host context carrying the session store.
488
+ */
489
+ constructor(ctx) {
490
+ super(ctx, "messageTools");
491
+ __runInitializers(this, _instanceExtraInitializers);
492
+ }
493
+ /**
494
+ * Withdraw a user message: replace it and every surface node after it with
495
+ * a minimal plugin-sourced placeholder, hiding the span from the model.
496
+ * @param request - session identity and target message seq.
497
+ * @returns the replacement receipt, or a rejection from the closed failure union.
498
+ */
499
+ async withdraw(request) {
500
+ const session = this.ctx.sessions.get(request.sessionId);
501
+ if (session === void 0) return {
502
+ ok: false,
503
+ error: { code: "session-not-found" }
504
+ };
505
+ const planned = planWithdrawal(session.events, session.surface.nodes, request.targetSeq);
506
+ if (!planned.ok) return {
507
+ ok: false,
508
+ error: { code: planned.code }
509
+ };
510
+ const replacement = session.append("user/message", createUserMessage({
511
+ content: [{
512
+ type: "text",
513
+ text: WITHDRAWN_NOTICE
514
+ }],
515
+ source: {
516
+ kind: "plugin",
517
+ plugin: MESSAGE_TOOLS_PLUGIN
518
+ }
519
+ }), {
520
+ surfaceOp: {
521
+ op: "replace",
522
+ start: planned.plan.start,
523
+ end: planned.plan.end
524
+ },
525
+ sourceEventSeqs: [...planned.plan.sourceEventSeqs]
526
+ });
527
+ await this.ctx.sessions.flush(session);
528
+ return {
529
+ ok: true,
530
+ value: {
531
+ replacementSeq: replacement.seq,
532
+ shadowedCount: planned.plan.sourceEventSeqs.length
533
+ }
534
+ };
535
+ }
536
+ /**
537
+ * Restore a withdrawn span: replay every replayable entry of it (user
538
+ * messages verbatim — including the last edit's new text — and framed
539
+ * assistant text; tool calls/results never replay) as fresh plugin-tagged
540
+ * `user/message` appends at the conversation tail, in original order, each
541
+ * citing its original event in `sourceEventSeqs`. The surface replace model
542
+ * is positional — a replaced span folds into one node — so the span cannot
543
+ * be restored in place; the replayed entries are simply the newest messages
544
+ * the model sees. Assistant text replays as framed user-role messages:
545
+ * `assistant/message` cannot carry a plugin source and the turn/step trace
546
+ * forbids assistant appends outside a step.
547
+ * @param request - session identity and withdrawn message seq.
548
+ * @returns the restore receipt, or a rejection from the closed failure union.
549
+ */
550
+ async restore(request) {
551
+ const session = this.ctx.sessions.get(request.sessionId);
552
+ if (session === void 0) return {
553
+ ok: false,
554
+ error: { code: "session-not-found" }
555
+ };
556
+ const planned = planRestore(session.events, session.surface.nodes, request.targetSeq);
557
+ if (!planned.ok) return {
558
+ ok: false,
559
+ error: { code: planned.code }
560
+ };
561
+ const appendedSeqs = [];
562
+ for (const entry of planned.plan.entries) {
563
+ const message = entry.role === "user" ? createUserMessage({
564
+ content: [...entry.content],
565
+ source: {
566
+ kind: "plugin",
567
+ plugin: MESSAGE_TOOLS_PLUGIN
568
+ }
569
+ }) : createUserMessage({
570
+ content: [{
571
+ type: "text",
572
+ text: entry.text
573
+ }],
574
+ source: restoreAssistantSource()
575
+ });
576
+ appendedSeqs.push(session.append("user/message", message, {
577
+ surfaceOp: "append",
578
+ sourceEventSeqs: [entry.sourceSeq]
579
+ }).seq);
580
+ }
581
+ await this.ctx.sessions.flush(session);
582
+ /* v8 ignore next -- entries is never empty (the target itself always replays), so [0] exists */
583
+ return {
584
+ ok: true,
585
+ value: { restoredSeq: appendedSeqs[0] ?? request.targetSeq }
586
+ };
587
+ }
588
+ /**
589
+ * Edit a user message in place: replace it and every surface node after it
590
+ * with the edited text itself (no placeholder, no tail duplicate — the
591
+ * edited text appears exactly once, in the replacement event), then start a
592
+ * regeneration turn. The harness has no wake-without-append path, so the
593
+ * turn is started by `agent.followup` carrying a minimal plugin-sourced
594
+ * trigger message (the ankh-guard/schedule pattern); the trigger is not the
595
+ * edited text and lands once, as itself.
596
+ * @param request - session identity, target seq, edited text.
597
+ * @returns the replacement receipt and whether the turn was triggered, or a rejection.
598
+ */
599
+ async edit(request) {
600
+ const session = this.ctx.sessions.get(request.sessionId);
601
+ if (session === void 0) return {
602
+ ok: false,
603
+ error: { code: "session-not-found" }
604
+ };
605
+ const planned = planEdit(session.events, session.surface.nodes, request.targetSeq, request.text);
606
+ if (!planned.ok) return {
607
+ ok: false,
608
+ error: { code: planned.code }
609
+ };
610
+ const replacement = session.append("user/message", createUserMessage({
611
+ content: [{
612
+ type: "text",
613
+ text: request.text
614
+ }],
615
+ source: editReplacementSource()
616
+ }), {
617
+ surfaceOp: {
618
+ op: "replace",
619
+ start: planned.plan.start,
620
+ end: planned.plan.end
621
+ },
622
+ sourceEventSeqs: [...planned.plan.sourceEventSeqs]
623
+ });
624
+ await this.ctx.sessions.flush(session);
625
+ const agent = this.ctx.agents.get(request.sessionId);
626
+ if (agent !== void 0) agent.followup(createUserMessage({
627
+ content: [{
628
+ type: "text",
629
+ text: EDIT_TRIGGER_NOTICE
630
+ }],
631
+ source: editTriggerSource()
632
+ }));
633
+ return {
634
+ ok: true,
635
+ value: {
636
+ replacementSeq: replacement.seq,
637
+ triggered: agent !== void 0
638
+ }
639
+ };
640
+ }
641
+ };
642
+ })();
643
+ //#endregion
644
+ export { MessageToolsService, MessageToolsService as default };
@@ -0,0 +1,27 @@
1
+ //#region lib/types/invariant.js
2
+ /**
3
+ * Package-owned invariant companion for `@khorsheed/dsh-client-message-tools`.
4
+ * @module @khorsheed/dsh-client-message-tools/invariant
5
+ */
6
+ const PACKAGE_NAME = "@khorsheed/dsh-client-message-tools";
7
+ /** Cordis companion plugin name. */
8
+ const name = "client-message-tools-invariant";
9
+ /** Service required before the companion can reserve package ownership. */
10
+ const inject = ["invariants"];
11
+ /**
12
+ * No runtime invariant: the withdrawal write passes through Session.append,
13
+ * whose surface validation rejects any replacement that misses a shadowed
14
+ * surface node, and the client contributions (one Definition, two slot
15
+ * entries) ride the slot/declaration lifecycles disposed with the plugin
16
+ * fiber. The package's unit tests exercise the plan fold and the projection
17
+ * helpers, so no second authority exists to check at runtime.
18
+ */
19
+ const install = () => {};
20
+ /**
21
+ * Register this package's invariant companion.
22
+ * @param ctx - Cordis context carrying the invariant service.
23
+ * @returns the installed registration's disposer after setup succeeds.
24
+ */
25
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
26
+ //#endregion
27
+ export { apply, inject, name };