@khorsheed/dsh-client-message-tools 0.3.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.
@@ -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/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Message Tools",
4
+ "description": "Edit or withdraw sent messages; withdrawal truly leaves the model context."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "消息工具",
4
+ "description": "编辑或撤回已发送的消息,撤回内容真正离开模型上下文。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "cordis",
8
8
  "messages"
9
9
  ],
10
- "version": "0.3.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": {
@@ -54,10 +55,13 @@
54
55
  },
55
56
  "compat": {
56
57
  "minHost": "0.1.5-rc.1",
57
- "verifiedHost": "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.5-rc.1",
78
- "@deepseek-ai/dsh-api-remotes": "^0.1.5-rc.1",
79
- "@deepseek-ai/dsh-api-session-controller": "^0.1.5-rc.1",
80
- "@deepseek-ai/dsh-client-locale": "^0.1.5-rc.1",
81
- "@deepseek-ai/dsh-client-store": "^0.1.5-rc.1",
82
- "@deepseek-ai/dsh-client-test-runtime": "^0.1.5-rc.1",
83
- "@deepseek-ai/dsh-client-ui-chat": "^0.1.5-rc.1",
84
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-rc.1",
85
- "@deepseek-ai/dsh-client-ui-model-selection": "^0.1.5-rc.1",
86
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.1",
87
- "@deepseek-ai/dsh-client-ui-renderer": "^0.1.5-rc.1",
88
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.1",
89
- "@deepseek-ai/dsh-invariants": "^0.1.5-rc.1",
90
- "@deepseek-ai/dsh-llm": "^0.1.5-rc.1",
91
- "@deepseek-ai/dsh-session": "^0.1.5-rc.1",
92
- "@deepseek-ai/dsh-typert-protocol": "^0.1.5-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
  }