@khorsheed/dsh-client-message-tools 0.2.0 → 0.3.1
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/CHANGELOG.md +21 -0
- package/README.en.md +10 -10
- package/README.i18n.yaml +2 -2
- package/README.md +9 -9
- package/lib/client.js +285 -112
- package/lib/index.js +238 -102
- package/lib/typert.host.js +40 -28
- package/lib/typert.remote-client.js +33 -21
- package/lib/types/client/ModelChip.js +4 -4
- package/lib/types/client/RestoredMessageView.js +2 -2
- package/lib/types/client/UserMessageView.js +11 -7
- package/lib/types/client/WithdrawnDividerView.d.ts +3 -2
- package/lib/types/client/WithdrawnDividerView.js +11 -5
- package/lib/types/client/chat-hook.js +1 -0
- package/lib/types/client/dom-hider.js +11 -4
- package/lib/types/client/icons-local.d.ts +23 -0
- package/lib/types/client/icons-local.js +11 -0
- package/lib/types/client/icons.d.ts +18 -19
- package/lib/types/client/icons.js +5 -10
- package/lib/types/client/index.js +6 -1
- package/lib/types/client/withdrawn-node.d.ts +17 -10
- package/lib/types/client/withdrawn-node.js +40 -22
- package/lib/types/index.d.ts +11 -11
- package/lib/types/index.js +21 -16
- package/lib/types/marker.d.ts +47 -15
- package/lib/types/marker.js +57 -30
- package/lib/types/restore-projection.d.ts +55 -0
- package/lib/types/restore-projection.js +123 -0
- package/lib/types/withdraw.js +67 -75
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +9 -28
- package/lib/client.js.map +0 -1
- package/lib/tsconfig.client.tsbuildinfo +0 -1
- package/lib/tsconfig.host.tsbuildinfo +0 -1
- package/lib/typert.remote-client.d.ts.map +0 -1
- package/lib/types/client/ModelChip.d.ts.map +0 -1
- package/lib/types/client/RestoredMessageView.d.ts.map +0 -1
- package/lib/types/client/UserMessageView.d.ts.map +0 -1
- package/lib/types/client/WithdrawnDividerView.d.ts.map +0 -1
- package/lib/types/client/backfill.d.ts.map +0 -1
- package/lib/types/client/chat-hook.d.ts.map +0 -1
- package/lib/types/client/dom-hider.d.ts.map +0 -1
- package/lib/types/client/edit-in-place.d.ts.map +0 -1
- package/lib/types/client/icons.d.ts.map +0 -1
- package/lib/types/client/index.d.ts.map +0 -1
- package/lib/types/client/locales.d.ts.map +0 -1
- package/lib/types/client/slots.d.ts.map +0 -1
- package/lib/types/client/withdraw-backfill.d.ts.map +0 -1
- package/lib/types/client/withdrawn-node.d.ts.map +0 -1
- package/lib/types/index.d.ts.map +0 -1
- package/lib/types/invariant.d.ts.map +0 -1
- package/lib/types/marker.d.ts.map +0 -1
- package/lib/types/types.d.ts.map +0 -1
- package/lib/types/withdraw.d.ts.map +0 -1
package/lib/types/marker.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared message-tools markers: the
|
|
3
|
-
*
|
|
4
|
-
* triggers, and assistant-text restore replays apart from
|
|
5
|
-
* user-message restores, the model-facing placeholder and
|
|
6
|
-
* the pure predicates both the Host service and the
|
|
7
|
-
* consume. No new session event type is ever written — an
|
|
2
|
+
* Shared message-tools markers: the producer-owned source kind stamped on
|
|
3
|
+
* every message-tools event, the `op` discriminator telling edit
|
|
4
|
+
* replacements, edit triggers, and assistant-text restore replays apart from
|
|
5
|
+
* withdrawals and user-message restores, the model-facing placeholder and
|
|
6
|
+
* trigger texts, and the pure predicates both the Host service and the
|
|
7
|
+
* browser projection consume. No new session event type is ever written — an
|
|
8
8
|
* out-of-harness event type cannot carry `ignorable: true` (Session.append
|
|
9
9
|
* assigns the envelope), so a persisted unknown type would make
|
|
10
10
|
* session-persistence refuse the log on reload. The events themselves are the
|
|
@@ -12,7 +12,14 @@
|
|
|
12
12
|
* @module @khorsheed/dsh-client-message-tools/marker
|
|
13
13
|
*/
|
|
14
14
|
import { isReplacementSurfaceEvent } from '@deepseek-ai/dsh-session/surface';
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* The producer-owned source kind identifying every message-tools event. It is
|
|
17
|
+
* also the legacy wrapper's `plugin` value: released V3 logs carry
|
|
18
|
+
* `{ kind: 'plugin', plugin: 'message-tools' }` (a pre-V4 host still serves
|
|
19
|
+
* them verbatim), and the official V3→V4 migration rewrites those to
|
|
20
|
+
* `kind: 'plugin:message-tools'` — {@link isMessageToolsSource} accepts all
|
|
21
|
+
* three durable forms.
|
|
22
|
+
*/
|
|
16
23
|
export const MESSAGE_TOOLS_PLUGIN = 'message-tools';
|
|
17
24
|
/**
|
|
18
25
|
* Model-facing placeholder left on the surface in place of the withdrawn
|
|
@@ -29,28 +36,52 @@ export const WITHDRAWN_NOTICE = '(用户撤回了这条消息及其后的所有
|
|
|
29
36
|
export const EDIT_TRIGGER_NOTICE = '(用户编辑了上一条消息,请按编辑后的内容重新回答)';
|
|
30
37
|
/**
|
|
31
38
|
* Model-facing frame preceding a replayed assistant text in a restore: the
|
|
32
|
-
* replay lands as a user-role message (assistant/message
|
|
33
|
-
*
|
|
34
|
-
* appends), so the frame keeps the role honest for the model.
|
|
39
|
+
* replay lands as a user-role message (assistant/message requires the model
|
|
40
|
+
* source, and the session turn/step trace forbids ad-hoc assistant
|
|
41
|
+
* appends), so the frame keeps the role honest for the model. On hosts whose
|
|
42
|
+
* session fold carries the S12 `user/message` projection the frame never
|
|
43
|
+
* reaches the model — the projection strips it and corrects the role at
|
|
44
|
+
* derivation time; everywhere else it is the role-honesty device.
|
|
35
45
|
*/
|
|
36
46
|
export const RESTORED_ASSISTANT_NOTICE = '(以下是先前被撤回、现随恢复放回的助手回复)';
|
|
37
|
-
/** The
|
|
47
|
+
/** The producer-owned source of a withdrawal replacement or a user-message restore replay (no op). */
|
|
48
|
+
export function messageToolsSource() {
|
|
49
|
+
return Object.freeze({ kind: MESSAGE_TOOLS_PLUGIN });
|
|
50
|
+
}
|
|
51
|
+
/** The producer-owned source of an edit replacement (op: 'edit'). */
|
|
38
52
|
export function editReplacementSource() {
|
|
39
|
-
return Object.freeze({ kind:
|
|
53
|
+
return Object.freeze({ kind: MESSAGE_TOOLS_PLUGIN, op: 'edit' });
|
|
40
54
|
}
|
|
41
|
-
/** The
|
|
55
|
+
/** The producer-owned source of an edit trigger message (op: 'edit-trigger'). */
|
|
42
56
|
export function editTriggerSource() {
|
|
43
|
-
return Object.freeze({ kind:
|
|
57
|
+
return Object.freeze({ kind: MESSAGE_TOOLS_PLUGIN, op: 'edit-trigger' });
|
|
44
58
|
}
|
|
45
|
-
/** The
|
|
59
|
+
/** The producer-owned source of a restored assistant-text replay (op: 'restore-assistant'). */
|
|
46
60
|
export function restoreAssistantSource() {
|
|
47
|
-
return Object.freeze({ kind:
|
|
61
|
+
return Object.freeze({ kind: MESSAGE_TOOLS_PLUGIN, op: 'restore-assistant' });
|
|
48
62
|
}
|
|
49
|
-
/** Read the op discriminator off a message source (absent on
|
|
63
|
+
/** Read the op discriminator off a message source (absent on plain withdrawal/restore events). */
|
|
50
64
|
export function messageToolsOp(source) {
|
|
51
65
|
const op = source?.op;
|
|
52
66
|
return op === 'edit' || op === 'edit-trigger' || op === 'restore-assistant' ? op : undefined;
|
|
53
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Whether a durable source belongs to message-tools, in every form the log
|
|
70
|
+
* can hold: the current producer-owned kind (`message-tools`), the V3→V4
|
|
71
|
+
* migrated kind (`plugin:message-tools` — released V3 files rewritten once by
|
|
72
|
+
* the official migration), and the released V3 wrapper a pre-V4 host still
|
|
73
|
+
* serves verbatim (`{ kind: 'plugin', plugin: 'message-tools' }`).
|
|
74
|
+
* @param source - the persisted message source to test.
|
|
75
|
+
* @returns true when the source names this producer.
|
|
76
|
+
*/
|
|
77
|
+
export function isMessageToolsSource(source) {
|
|
78
|
+
if (typeof source !== 'object' || source === null)
|
|
79
|
+
return false;
|
|
80
|
+
const record = source;
|
|
81
|
+
return record.kind === MESSAGE_TOOLS_PLUGIN
|
|
82
|
+
|| record.kind === `plugin:${MESSAGE_TOOLS_PLUGIN}`
|
|
83
|
+
|| (record.kind === 'plugin' && record.plugin === MESSAGE_TOOLS_PLUGIN);
|
|
84
|
+
}
|
|
54
85
|
/**
|
|
55
86
|
* Strip the model-facing restore frame from a replayed assistant text for
|
|
56
87
|
* DISPLAY: the frame belongs to the model context (role honesty), never to
|
|
@@ -64,16 +95,16 @@ export function stripRestoreAssistantFrame(text) {
|
|
|
64
95
|
}
|
|
65
96
|
/**
|
|
66
97
|
* Whether the event is a message-tools WITHDRAWAL replacement: a
|
|
67
|
-
* `user/message` surface replacement whose source
|
|
68
|
-
*
|
|
98
|
+
* `user/message` surface replacement whose source names this producer
|
|
99
|
+
* (any durable form — see {@link isMessageToolsSource}) with no `op`
|
|
100
|
+
* discriminator.
|
|
69
101
|
* @param event - the session event to test.
|
|
70
102
|
* @returns the event narrowed to a withdrawal replacement when matched.
|
|
71
103
|
*/
|
|
72
104
|
export function isMessageToolsReplacement(event) {
|
|
73
105
|
return event.type === 'user/message'
|
|
74
106
|
&& isReplacementSurfaceEvent(event)
|
|
75
|
-
&& event.data.source
|
|
76
|
-
&& event.data.source.plugin === MESSAGE_TOOLS_PLUGIN
|
|
107
|
+
&& isMessageToolsSource(event.data.source)
|
|
77
108
|
&& messageToolsOp(event.data.source) === undefined;
|
|
78
109
|
}
|
|
79
110
|
/**
|
|
@@ -85,8 +116,7 @@ export function isMessageToolsReplacement(event) {
|
|
|
85
116
|
export function isMessageToolsEdit(event) {
|
|
86
117
|
return event.type === 'user/message'
|
|
87
118
|
&& isReplacementSurfaceEvent(event)
|
|
88
|
-
&& event.data.source
|
|
89
|
-
&& event.data.source.plugin === MESSAGE_TOOLS_PLUGIN
|
|
119
|
+
&& isMessageToolsSource(event.data.source)
|
|
90
120
|
&& messageToolsOp(event.data.source) === 'edit';
|
|
91
121
|
}
|
|
92
122
|
/**
|
|
@@ -98,21 +128,19 @@ export function isMessageToolsEdit(event) {
|
|
|
98
128
|
export function isMessageToolsRestore(event) {
|
|
99
129
|
return event.type === 'user/message'
|
|
100
130
|
&& event.surfaceOp === 'append'
|
|
101
|
-
&& event.data.source
|
|
102
|
-
&& event.data.source.plugin === MESSAGE_TOOLS_PLUGIN
|
|
131
|
+
&& isMessageToolsSource(event.data.source)
|
|
103
132
|
&& messageToolsOp(event.data.source) === undefined;
|
|
104
133
|
}
|
|
105
134
|
/**
|
|
106
135
|
* Whether the event is a message-tools edit trigger: the minimal
|
|
107
|
-
*
|
|
136
|
+
* producer-sourced message that starts the regeneration turn after an edit.
|
|
108
137
|
* @param event - the session event to test.
|
|
109
138
|
* @returns the event narrowed to an edit trigger when matched.
|
|
110
139
|
*/
|
|
111
140
|
export function isMessageToolsTrigger(event) {
|
|
112
141
|
return event.type === 'user/message'
|
|
113
142
|
&& event.surfaceOp === 'append'
|
|
114
|
-
&& event.data.source
|
|
115
|
-
&& event.data.source.plugin === MESSAGE_TOOLS_PLUGIN
|
|
143
|
+
&& isMessageToolsSource(event.data.source)
|
|
116
144
|
&& messageToolsOp(event.data.source) === 'edit-trigger';
|
|
117
145
|
}
|
|
118
146
|
/**
|
|
@@ -125,7 +153,6 @@ export function isMessageToolsTrigger(event) {
|
|
|
125
153
|
export function isMessageToolsRestoreAssistant(event) {
|
|
126
154
|
return event.type === 'user/message'
|
|
127
155
|
&& event.surfaceOp === 'append'
|
|
128
|
-
&& event.data.source
|
|
129
|
-
&& event.data.source.plugin === MESSAGE_TOOLS_PLUGIN
|
|
156
|
+
&& isMessageToolsSource(event.data.source)
|
|
130
157
|
&& messageToolsOp(event.data.source) === 'restore-assistant';
|
|
131
158
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Restore-assistant message projection (seam registry S12): the pure
|
|
3
|
+
* `user/message` interpreter that rewrites a restore replay's derived message
|
|
4
|
+
* to assistant role with the model-facing frame stripped, plus the
|
|
5
|
+
* registration helper that probes the host for the capability and degrades to
|
|
6
|
+
* the framed user-role channel when it cannot carry the projection. The
|
|
7
|
+
* durable log is identical on both channels — the restore append keeps its
|
|
8
|
+
* `restore-assistant` source mark and its frame prefix; only the model-facing
|
|
9
|
+
* derivation differs.
|
|
10
|
+
* @module @khorsheed/dsh-client-message-tools/restore-projection
|
|
11
|
+
*/
|
|
12
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
13
|
+
import * as sessionApi from '@deepseek-ai/dsh-session';
|
|
14
|
+
import type { SessionMessageProjection } from '@deepseek-ai/dsh-session';
|
|
15
|
+
/**
|
|
16
|
+
* The `user/message` projection correcting restore replays at derivation
|
|
17
|
+
* time. It registers on the SHARED user-message vocabulary (a custom event
|
|
18
|
+
* type could never persist — S2), so the fold calls `project` synchronously
|
|
19
|
+
* for EVERY user message, inside append validation: it must be a pure
|
|
20
|
+
* pass-through (empty map) for anything without the restore mark, and it must
|
|
21
|
+
* NEVER throw — a throw atomically rejects the candidate append, so a
|
|
22
|
+
* throwing projection would break ordinary user messages instance-wide. Any
|
|
23
|
+
* malformed durable payload therefore also yields an empty map.
|
|
24
|
+
*/
|
|
25
|
+
export declare const restoreAssistantProjection: SessionMessageProjection<'user/message'>;
|
|
26
|
+
/**
|
|
27
|
+
* Whether this host's surface fold can carry a `user/message` projection at
|
|
28
|
+
* all. 0.1.6-alpha.1 routes every event of a projected type through the
|
|
29
|
+
* projection plan and never joins it to the surface (probe-verified): a
|
|
30
|
+
* registered user/message projection would silently drop EVERY user message
|
|
31
|
+
* from the model context. This probe folds two synthetic events through the
|
|
32
|
+
* real `foldSurface` and accepts only the composing semantics the design
|
|
33
|
+
* needs — plain messages keep their node, and a restore replay keeps its
|
|
34
|
+
* node AND derives assistant-role with the frame stripped — so the channel
|
|
35
|
+
* stays dormant on 0.1.6-alpha.1 and lights up by itself on a host whose
|
|
36
|
+
* fold composes. Pure, synchronous, side-effect-free.
|
|
37
|
+
* @param fold - the host's surface fold, injectable for tests.
|
|
38
|
+
* @param derive - the host's single-event derivation, injectable for tests.
|
|
39
|
+
* @returns true when a registered user/message projection is safe.
|
|
40
|
+
*/
|
|
41
|
+
export declare function restoreProjectionFoldSupported(fold?: typeof sessionApi.foldSurface | undefined, derive?: typeof sessionApi.deriveEventMessage | undefined): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Probe and register the restore-assistant projection, degrading to the
|
|
44
|
+
* framed user-role channel (which the durable events already carry) whenever
|
|
45
|
+
* the host cannot carry the projection: pre-0.1.6 (`registerMessageProjection`
|
|
46
|
+
* absent), a fold that cannot compose projections with surface membership, or
|
|
47
|
+
* a registration conflict (the projection slot is one-per-type per instance —
|
|
48
|
+
* a future official projection claiming user/message throws, and the plugin
|
|
49
|
+
* must not take the instance down with it).
|
|
50
|
+
* @param ctx - host context carrying the session store.
|
|
51
|
+
* @param foldSupported - fold-semantics probe, injectable for tests.
|
|
52
|
+
* @returns true when the projection channel is active.
|
|
53
|
+
*/
|
|
54
|
+
export declare function registerRestoreProjection(ctx: Context, foldSupported?: () => boolean): boolean;
|
|
55
|
+
//# sourceMappingURL=restore-projection.d.ts.map
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
2
|
+
// Value: the branded seq constructor the probe events require.
|
|
3
|
+
import { SessionSeq } from '@deepseek-ai/dsh-session';
|
|
4
|
+
// Namespace: foldSurface/deriveEventMessage are 0.1.6 additions — a named
|
|
5
|
+
// import would fail module linking on a 0.1.5 host, so the probe reaches them
|
|
6
|
+
// through the namespace object (absent members read as undefined).
|
|
7
|
+
import * as sessionApi from '@deepseek-ai/dsh-session';
|
|
8
|
+
import { RESTORED_ASSISTANT_NOTICE, messageToolsOp, restoreAssistantSource, stripRestoreAssistantFrame, } from './marker.js';
|
|
9
|
+
/**
|
|
10
|
+
* The `user/message` projection correcting restore replays at derivation
|
|
11
|
+
* time. It registers on the SHARED user-message vocabulary (a custom event
|
|
12
|
+
* type could never persist — S2), so the fold calls `project` synchronously
|
|
13
|
+
* for EVERY user message, inside append validation: it must be a pure
|
|
14
|
+
* pass-through (empty map) for anything without the restore mark, and it must
|
|
15
|
+
* NEVER throw — a throw atomically rejects the candidate append, so a
|
|
16
|
+
* throwing projection would break ordinary user messages instance-wide. Any
|
|
17
|
+
* malformed durable payload therefore also yields an empty map.
|
|
18
|
+
*/
|
|
19
|
+
export const restoreAssistantProjection = {
|
|
20
|
+
type: 'user/message',
|
|
21
|
+
project(event, context) {
|
|
22
|
+
try {
|
|
23
|
+
const source = event.data?.source;
|
|
24
|
+
if (messageToolsOp(source) !== 'restore-assistant')
|
|
25
|
+
return new Map();
|
|
26
|
+
// The current message at the candidate's own seq: a prior projection's
|
|
27
|
+
// rewrite wins, then the committed window entry (fold replay), then the
|
|
28
|
+
// candidate's own data (live append validation, where the window
|
|
29
|
+
// excludes the candidate). Mirrors the official image-offload
|
|
30
|
+
// projection's message fetch.
|
|
31
|
+
const committed = context.events[event.seq - context.baseSeq];
|
|
32
|
+
const message = context.messages.get(event.seq)
|
|
33
|
+
?? (committed?.type === 'user/message' ? committed.data : undefined)
|
|
34
|
+
?? event.data;
|
|
35
|
+
const content = message.content.map(block => block.type === 'text' && typeof block.text === 'string'
|
|
36
|
+
? { ...block, text: stripRestoreAssistantFrame(block.text) }
|
|
37
|
+
: block);
|
|
38
|
+
return new Map([[event.seq, { ...message, role: 'assistant', content }]]);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return new Map();
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
/** One synthetic user/message event for the fold-semantics probe. */
|
|
46
|
+
function probeEvent(seq, text, source) {
|
|
47
|
+
return {
|
|
48
|
+
type: 'user/message',
|
|
49
|
+
seq: SessionSeq(seq),
|
|
50
|
+
time: 0,
|
|
51
|
+
data: createUserMessage({ content: [{ type: 'text', text }], source }),
|
|
52
|
+
surfaceOp: 'append',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Whether this host's surface fold can carry a `user/message` projection at
|
|
57
|
+
* all. 0.1.6-alpha.1 routes every event of a projected type through the
|
|
58
|
+
* projection plan and never joins it to the surface (probe-verified): a
|
|
59
|
+
* registered user/message projection would silently drop EVERY user message
|
|
60
|
+
* from the model context. This probe folds two synthetic events through the
|
|
61
|
+
* real `foldSurface` and accepts only the composing semantics the design
|
|
62
|
+
* needs — plain messages keep their node, and a restore replay keeps its
|
|
63
|
+
* node AND derives assistant-role with the frame stripped — so the channel
|
|
64
|
+
* stays dormant on 0.1.6-alpha.1 and lights up by itself on a host whose
|
|
65
|
+
* fold composes. Pure, synchronous, side-effect-free.
|
|
66
|
+
* @param fold - the host's surface fold, injectable for tests.
|
|
67
|
+
* @param derive - the host's single-event derivation, injectable for tests.
|
|
68
|
+
* @returns true when a registered user/message projection is safe.
|
|
69
|
+
*/
|
|
70
|
+
export function restoreProjectionFoldSupported(fold = sessionApi.foldSurface, derive = sessionApi.deriveEventMessage) {
|
|
71
|
+
try {
|
|
72
|
+
if (typeof fold !== 'function' || typeof derive !== 'function')
|
|
73
|
+
return false;
|
|
74
|
+
const events = [
|
|
75
|
+
probeEvent(0, 'probe', { kind: 'user' }),
|
|
76
|
+
probeEvent(1, `${RESTORED_ASSISTANT_NOTICE}\nprobe answer`, restoreAssistantSource()),
|
|
77
|
+
];
|
|
78
|
+
const folded = fold(events, [restoreAssistantProjection]);
|
|
79
|
+
if (folded.nodes.length !== events.length)
|
|
80
|
+
return false;
|
|
81
|
+
const derived = derive(events[1], folded.projectedMessages);
|
|
82
|
+
return derived?.role === 'assistant'
|
|
83
|
+
&& derived.content.some(block => block.type === 'text' && block.text === 'probe answer');
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Probe and register the restore-assistant projection, degrading to the
|
|
91
|
+
* framed user-role channel (which the durable events already carry) whenever
|
|
92
|
+
* the host cannot carry the projection: pre-0.1.6 (`registerMessageProjection`
|
|
93
|
+
* absent), a fold that cannot compose projections with surface membership, or
|
|
94
|
+
* a registration conflict (the projection slot is one-per-type per instance —
|
|
95
|
+
* a future official projection claiming user/message throws, and the plugin
|
|
96
|
+
* must not take the instance down with it).
|
|
97
|
+
* @param ctx - host context carrying the session store.
|
|
98
|
+
* @param foldSupported - fold-semantics probe, injectable for tests.
|
|
99
|
+
* @returns true when the projection channel is active.
|
|
100
|
+
*/
|
|
101
|
+
export function registerRestoreProjection(ctx, foldSupported = restoreProjectionFoldSupported) {
|
|
102
|
+
const sessions = ctx.sessions;
|
|
103
|
+
if (typeof sessions.registerMessageProjection !== 'function') {
|
|
104
|
+
ctx.logger.info('message-tools: host has no sessions.registerMessageProjection — restored assistant text stays in the framed user-role channel');
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
if (!foldSupported()) {
|
|
108
|
+
ctx.logger.warn('message-tools: host message-projection fold drops projected user/message events from the surface — restored assistant text stays in the framed user-role channel');
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const dispose = sessions.registerMessageProjection(restoreAssistantProjection);
|
|
113
|
+
// The host's returned disposer is fiber-owned and idempotent (cordis
|
|
114
|
+
// effect disposal re-entry is a no-op); wiring it here as well keeps
|
|
115
|
+
// unregister-on-unload exact even if a future host binds it elsewhere.
|
|
116
|
+
ctx.effect(() => () => dispose(), 'message-tools: restore-assistant projection');
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
ctx.logger.warn('message-tools: a user/message message-projection is already registered — restored assistant text stays in the framed user-role channel');
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
package/lib/types/withdraw.js
CHANGED
|
@@ -111,10 +111,10 @@ function joinAssistantMessageText(blocks) {
|
|
|
111
111
|
* (the message left the surface through another producer, e.g. compaction).
|
|
112
112
|
*
|
|
113
113
|
* `sourceEventSeqs` is authoritative for the shadowed *surface* nodes, but an
|
|
114
|
-
* interrupted assistant step may exist only as `assistant/
|
|
115
|
-
* `assistant/message` ever landed).
|
|
116
|
-
* they are absent from `sourceEventSeqs`. The replacement's own `
|
|
117
|
-
* `seq` delimit the whole withdrawn log interval — `[
|
|
114
|
+
* interrupted assistant step may exist only as `assistant/attempt` events (no
|
|
115
|
+
* `assistant/message` ever landed). Attempts are not surface nodes, so
|
|
116
|
+
* they are absent from `sourceEventSeqs`. The replacement's own `startSeq` and
|
|
117
|
+
* `seq` delimit the whole withdrawn log interval — `[startSeq, seq)` — and are
|
|
118
118
|
* still read from the replacement itself, never guessed from another producer.
|
|
119
119
|
*/
|
|
120
120
|
function findWithdrawnSpan(events, targetSeq) {
|
|
@@ -127,7 +127,7 @@ function findWithdrawnSpan(events, targetSeq) {
|
|
|
127
127
|
if (sources?.includes(targetSeq) !== true)
|
|
128
128
|
continue;
|
|
129
129
|
const span = [];
|
|
130
|
-
for (let seq = event.surfaceOp.
|
|
130
|
+
for (let seq = event.surfaceOp.startSeq; seq < event.seq; seq++)
|
|
131
131
|
span.push(seq);
|
|
132
132
|
return span;
|
|
133
133
|
}
|
|
@@ -140,64 +140,27 @@ function findWithdrawnSpan(events, targetSeq) {
|
|
|
140
140
|
* restore-assistant replays already carry the frame). Skipped: withdrawal
|
|
141
141
|
* placeholders, edit triggers, other plugin context, and tool calls/results.
|
|
142
142
|
*
|
|
143
|
-
* Assistant text normally comes from `assistant/message` surface events
|
|
144
|
-
* turn
|
|
145
|
-
* `
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
143
|
+
* Assistant text normally comes from `assistant/message` surface events (a
|
|
144
|
+
* cancelled turn finalizes its delivered prefix there with `interrupted:
|
|
145
|
+
* true`). An attempt that committed no surface message — failed, retried, or
|
|
146
|
+
* stream-error — settles as an `assistant/attempt` log event carrying its
|
|
147
|
+
* exact compact stream. Each such attempt folds into one assistant replay
|
|
148
|
+
* when no final `assistant/message` exists for that step, so
|
|
149
|
+
* withdrawing/restoring an interrupted reply does not lose the partial
|
|
150
|
+
* assistant content. A retried step's earlier attempts are superseded by the
|
|
151
|
+
* step's final message and do not replay.
|
|
149
152
|
*/
|
|
150
153
|
function replayEntries(events, spanSeqs) {
|
|
151
154
|
const entries = [];
|
|
152
|
-
|
|
155
|
+
// A step's final message supersedes its attempts; collect the finalized
|
|
156
|
+
// steps first so an attempt is skipped regardless of log order.
|
|
153
157
|
const finalizedSteps = new Set();
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const text = [...pending.text.values()].join('');
|
|
159
|
-
const reasoning = [...pending.reasoning.values()].join('');
|
|
160
|
-
const replay = text !== '' ? text : reasoning;
|
|
161
|
-
if (replay !== '') {
|
|
162
|
-
entries.push({
|
|
163
|
-
role: 'assistant',
|
|
164
|
-
text: `${RESTORED_ASSISTANT_NOTICE}\n${replay}`,
|
|
165
|
-
sourceSeq: pending.sourceSeq,
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
pending = undefined;
|
|
170
|
-
};
|
|
171
|
-
const pushChunk = (event) => {
|
|
172
|
-
if (finalizedSteps.has(`${event.data.turn}:${event.data.step}`))
|
|
173
|
-
return;
|
|
174
|
-
if (pending === undefined
|
|
175
|
-
|| pending.turn !== event.data.turn
|
|
176
|
-
|| pending.step !== event.data.step) {
|
|
177
|
-
flushPending();
|
|
178
|
-
pending = {
|
|
179
|
-
turn: event.data.turn,
|
|
180
|
-
step: event.data.step,
|
|
181
|
-
sourceSeq: event.seq,
|
|
182
|
-
text: new Map(),
|
|
183
|
-
reasoning: new Map(),
|
|
184
|
-
finalized: false,
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
const chunk = event.data.chunk;
|
|
188
|
-
if (chunk.type === 'text-delta') {
|
|
189
|
-
pending.text.set(chunk.index, (pending.text.get(chunk.index) ?? '') + chunk.text);
|
|
190
|
-
}
|
|
191
|
-
else if (chunk.type === 'block-end' && chunk.block.type === 'text') {
|
|
192
|
-
pending.text.set(chunk.index, chunk.block.text);
|
|
193
|
-
}
|
|
194
|
-
else if (chunk.type === 'reasoning-delta') {
|
|
195
|
-
pending.reasoning.set(chunk.index, (pending.reasoning.get(chunk.index) ?? '') + chunk.text);
|
|
196
|
-
}
|
|
197
|
-
else if (chunk.type === 'block-end' && chunk.block.type === 'reasoning') {
|
|
198
|
-
pending.reasoning.set(chunk.index, chunk.block.text);
|
|
158
|
+
for (const seq of spanSeqs) {
|
|
159
|
+
const event = events[seq];
|
|
160
|
+
if (event?.type === 'assistant/message') {
|
|
161
|
+
finalizedSteps.add(`${event.data.turn}:${event.data.step}`);
|
|
199
162
|
}
|
|
200
|
-
}
|
|
163
|
+
}
|
|
201
164
|
const pushAssistantText = (text, sourceSeq) => {
|
|
202
165
|
if (text !== '') {
|
|
203
166
|
entries.push({ role: 'assistant', text: `${RESTORED_ASSISTANT_NOTICE}\n${text}`, sourceSeq });
|
|
@@ -208,16 +171,16 @@ function replayEntries(events, spanSeqs) {
|
|
|
208
171
|
if (event === undefined)
|
|
209
172
|
continue;
|
|
210
173
|
if (isMessageToolsEdit(event)) {
|
|
211
|
-
flushPending();
|
|
212
174
|
entries.push({ role: 'user', content: event.data.content, sourceSeq: seq });
|
|
213
175
|
continue;
|
|
214
176
|
}
|
|
215
|
-
if (event.type === 'assistant/
|
|
216
|
-
|
|
177
|
+
if (event.type === 'assistant/attempt') {
|
|
178
|
+
if (finalizedSteps.has(`${event.data.turn}:${event.data.step}`))
|
|
179
|
+
continue;
|
|
180
|
+
pushAssistantText(joinAttemptStreamText(event.data.stream), seq);
|
|
217
181
|
continue;
|
|
218
182
|
}
|
|
219
183
|
if (event.type === 'user/message') {
|
|
220
|
-
flushPending();
|
|
221
184
|
if (event.surfaceOp !== 'append')
|
|
222
185
|
continue;
|
|
223
186
|
if (event.data.source.kind === 'user' || isMessageToolsRestore(event)) {
|
|
@@ -231,25 +194,54 @@ function replayEntries(events, spanSeqs) {
|
|
|
231
194
|
continue;
|
|
232
195
|
}
|
|
233
196
|
if (event.type === 'assistant/message') {
|
|
234
|
-
// A finalized step supersedes any pending chunk accumulation for the
|
|
235
|
-
// same step: flush without emitting the partial, then replay the final.
|
|
236
|
-
finalizedSteps.add(`${event.data.turn}:${event.data.step}`);
|
|
237
|
-
if (pending !== undefined
|
|
238
|
-
&& pending.turn === event.data.turn
|
|
239
|
-
&& pending.step === event.data.step) {
|
|
240
|
-
pending.finalized = true;
|
|
241
|
-
}
|
|
242
|
-
flushPending();
|
|
243
197
|
pushAssistantText(joinAssistantMessageText(event.data.message.content), seq);
|
|
244
198
|
continue;
|
|
245
199
|
}
|
|
246
|
-
// Log-only events (boundaries, headers, titles)
|
|
247
|
-
// the pending assistant step is flushed at the next replayable boundary or
|
|
248
|
-
// when a different step starts.
|
|
200
|
+
// Log-only events (boundaries, headers, titles) contribute nothing.
|
|
249
201
|
}
|
|
250
|
-
flushPending();
|
|
251
202
|
return entries;
|
|
252
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* Join one attempt's compact stream into its visible text. Text deltas win;
|
|
206
|
+
* reasoning is the fallback so an attempt that streamed only reasoning does
|
|
207
|
+
* not lose the only assistant content the user saw (the same preference
|
|
208
|
+
* {@link joinAssistantMessageText} applies to finalized messages). Packed
|
|
209
|
+
* delta runs append per block index; a raw `block-end` record carries the
|
|
210
|
+
* assembled block and replaces its index's accumulated deltas. Tool-call
|
|
211
|
+
* runs never replay (their side effects are not replayable).
|
|
212
|
+
*/
|
|
213
|
+
function joinAttemptStreamText(stream) {
|
|
214
|
+
const text = new Map();
|
|
215
|
+
const reasoning = new Map();
|
|
216
|
+
const append = (map, index, fragment) => {
|
|
217
|
+
map.set(index, (map.get(index) ?? '') + fragment);
|
|
218
|
+
};
|
|
219
|
+
for (const record of stream) {
|
|
220
|
+
if (record.type === 'text-chunks') {
|
|
221
|
+
append(text, record.index, record.texts.join(''));
|
|
222
|
+
}
|
|
223
|
+
else if (record.type === 'reasoning-chunks') {
|
|
224
|
+
append(reasoning, record.index, record.texts.join(''));
|
|
225
|
+
}
|
|
226
|
+
else if (record.type === 'chunk') {
|
|
227
|
+
const chunk = record.chunk;
|
|
228
|
+
if (chunk.type === 'text-delta') {
|
|
229
|
+
append(text, chunk.index, chunk.text);
|
|
230
|
+
}
|
|
231
|
+
else if (chunk.type === 'reasoning-delta') {
|
|
232
|
+
append(reasoning, chunk.index, chunk.text);
|
|
233
|
+
}
|
|
234
|
+
else if (chunk.type === 'block-end' && chunk.block.type === 'text') {
|
|
235
|
+
text.set(chunk.index, chunk.block.text);
|
|
236
|
+
}
|
|
237
|
+
else if (chunk.type === 'block-end' && chunk.block.type === 'reasoning') {
|
|
238
|
+
reasoning.set(chunk.index, chunk.block.text);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const joined = [...text.values()].join('');
|
|
243
|
+
return joined !== '' ? joined : [...reasoning.values()].join('');
|
|
244
|
+
}
|
|
253
245
|
/**
|
|
254
246
|
* Plan a restore: the target must be an editable user message (the same
|
|
255
247
|
* vocabulary as withdrawal — original, restore replay, or edit replacement)
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"cordis",
|
|
8
8
|
"messages"
|
|
9
9
|
],
|
|
10
|
-
"version": "0.
|
|
10
|
+
"version": "0.3.1",
|
|
11
11
|
"type": "module",
|
|
12
12
|
"main": "lib/index.js",
|
|
13
13
|
"types": "lib/types/index.d.ts",
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"default": "./lib/typert.remote-client.js"
|
|
38
38
|
},
|
|
39
39
|
"./src/*": "./src/*",
|
|
40
|
-
"./package.json": "./package.json"
|
|
40
|
+
"./package.json": "./package.json",
|
|
41
|
+
"./locale/*.json": "./locale/*.json"
|
|
41
42
|
},
|
|
42
43
|
"dsh": {
|
|
43
44
|
"bundle": {
|
|
@@ -53,11 +54,14 @@
|
|
|
53
54
|
"platform": "web"
|
|
54
55
|
},
|
|
55
56
|
"compat": {
|
|
56
|
-
"minHost": "0.1.
|
|
57
|
-
"verifiedHost": "0.1.
|
|
57
|
+
"minHost": "0.1.5-rc.1",
|
|
58
|
+
"verifiedHost": "0.1.7-rc.1"
|
|
58
59
|
}
|
|
59
60
|
},
|
|
60
61
|
"license": "MIT",
|
|
62
|
+
"dependencies": {
|
|
63
|
+
"zod": "^4.4.3"
|
|
64
|
+
},
|
|
61
65
|
"peerDependencies": {
|
|
62
66
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
63
67
|
"@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.6",
|
|
@@ -72,30 +76,6 @@
|
|
|
72
76
|
"@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.6",
|
|
73
77
|
"react": "^18.2.0"
|
|
74
78
|
},
|
|
75
|
-
"devDependencies": {
|
|
76
|
-
"@deepseek-ai/cordis": "^4.0.1",
|
|
77
|
-
"@deepseek-ai/dsh-agent": "^0.1.2-rc.1",
|
|
78
|
-
"@deepseek-ai/dsh-api-remotes": "^0.1.2-rc.1",
|
|
79
|
-
"@deepseek-ai/dsh-api-session-controller": "^0.1.2-rc.1",
|
|
80
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.2-rc.1",
|
|
81
|
-
"@deepseek-ai/dsh-client-store": "^0.1.2-rc.1",
|
|
82
|
-
"@deepseek-ai/dsh-client-test-runtime": "^0.1.2-rc.1",
|
|
83
|
-
"@deepseek-ai/dsh-client-ui-chat": "^0.1.2-rc.1",
|
|
84
|
-
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.2-rc.1",
|
|
85
|
-
"@deepseek-ai/dsh-client-ui-model-selection": "^0.1.2-rc.1",
|
|
86
|
-
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.2-rc.1",
|
|
87
|
-
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.2-rc.1",
|
|
88
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.2-rc.1",
|
|
89
|
-
"@deepseek-ai/dsh-invariants": "^0.1.2-rc.1",
|
|
90
|
-
"@deepseek-ai/dsh-llm": "^0.1.2-rc.1",
|
|
91
|
-
"@deepseek-ai/dsh-session": "^0.1.2-rc.1",
|
|
92
|
-
"@deepseek-ai/dsh-typert-protocol": "^0.1.2-rc.1",
|
|
93
|
-
"@testing-library/react": "^16.1.0",
|
|
94
|
-
"@types/node": "^26.2.0",
|
|
95
|
-
"@types/react": "~18.3.1",
|
|
96
|
-
"react": "^18.2.0",
|
|
97
|
-
"react-dom": "^18.2.0"
|
|
98
|
-
},
|
|
99
79
|
"files": [
|
|
100
80
|
"lib",
|
|
101
81
|
"cordis.patch.yml",
|
|
@@ -104,6 +84,7 @@
|
|
|
104
84
|
"lib/typert.remote-client.d.ts",
|
|
105
85
|
"lib/typert.remote-client.js",
|
|
106
86
|
"LICENSE",
|
|
87
|
+
"locale/*.json",
|
|
107
88
|
"CHANGELOG.md"
|
|
108
89
|
]
|
|
109
90
|
}
|