@everme/dsh 0.6.3 → 0.6.5
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 +47 -0
- package/index.js +123 -18
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -10,6 +10,53 @@ The Cordis plugin complements `@everme/memory-mcp`:
|
|
|
10
10
|
- Recall and save failures degrade open: DSH continues without blocking the user turn.
|
|
11
11
|
- The MCP server remains available through `npx -y @everme/memory-mcp@latest` for explicit read-only `mem_context` and `mem_search` tool calls (saving is automatic via the hooks).
|
|
12
12
|
|
|
13
|
+
## Message conversion
|
|
14
|
+
|
|
15
|
+
The native turn-end writer uses task-aware batching: preserve fitting tasks,
|
|
16
|
+
compress oversized tool results with 4/3/2 KiB budgets, then split only if
|
|
17
|
+
necessary. This does not alter the existing flush-on-turn-end contract.
|
|
18
|
+
Native child sessions without genuine human input are skipped at the writer.
|
|
19
|
+
After a native browser-authored follow-up, later assistant-only turns remain
|
|
20
|
+
eligible; root sessions are not subject to this child-only admission rule.
|
|
21
|
+
|
|
22
|
+
The native turn collector uses `turn/start` and `turn/end` from the session log.
|
|
23
|
+
Only `user/message` records whose source is `user` become human messages;
|
|
24
|
+
plugin context, reasoning, and control events do not become conversation text.
|
|
25
|
+
For native child sessions (`origin: subagent` with a v3 one-shot or continuable
|
|
26
|
+
descriptor), initial delegated tasks use a bare user source and are excluded.
|
|
27
|
+
Browser-authored follow-ups retain their native `source.rpcId` and are preserved.
|
|
28
|
+
This distinction does not depend on message position, text, or configurable
|
|
29
|
+
provider names. Root-session user messages and child assistant/tool activity
|
|
30
|
+
are unchanged; inherited history is not replayed as new turn content.
|
|
31
|
+
Trusted user text is extracted without channel-metadata pattern stripping, so
|
|
32
|
+
quoted Sender blocks, message IDs, and timestamps remain part of the dialogue.
|
|
33
|
+
Recall query cleaning is unchanged; ordinary write limits still apply.
|
|
34
|
+
Assistant tool-call blocks carry the provider's string ID and name. Arguments
|
|
35
|
+
must be a valid JSON string; the native runtime's explicit empty-string argument
|
|
36
|
+
form means no parameters and is encoded as `{}`. Missing fields are not filled in.
|
|
37
|
+
The separate `tool/call` log record is not uploaded a second time.
|
|
38
|
+
|
|
39
|
+
Tool results retain their native `toolCallId` and content array. An explicit
|
|
40
|
+
empty array remains one empty result; a missing content field is rejected.
|
|
41
|
+
Long tool results have no additional 8000-character cap. Normal text extraction,
|
|
42
|
+
ordinary-message limits, and the SDK's request budgets still apply. Invalid
|
|
43
|
+
native IDs, names, arguments, or result fields are counted in bounded diagnostics;
|
|
44
|
+
valid neighboring text is retained. A missing real session ID prevents the write.
|
|
45
|
+
When an invalid call has a unique native ID within the turn, its corresponding
|
|
46
|
+
result is excluded as well. Duplicate native IDs are ambiguous, so they do not
|
|
47
|
+
authorize guessing which result belongs to the rejected call.
|
|
48
|
+
|
|
49
|
+
Only an actual final assistant text message in a `completed` turn, without tool
|
|
50
|
+
calls or interruption, carries the internal completion marker. A tool can itself
|
|
51
|
+
conclude a native DSH turn, so `turn/end` does not authorize inventing a final
|
|
52
|
+
assistant message. Other assistant messages explicitly remain non-final. The
|
|
53
|
+
marker reaches the BFF for batching but is not sent to EverOS.
|
|
54
|
+
|
|
55
|
+
`collectTurnMessages` accepts an optional fourth logger argument for conversion
|
|
56
|
+
diagnostics; existing three-argument callers remain compatible. Historical
|
|
57
|
+
non-native object arguments, string result containers, and coerced IDs are not
|
|
58
|
+
part of the native DSH contract and are rejected rather than repaired.
|
|
59
|
+
|
|
13
60
|
## Install
|
|
14
61
|
|
|
15
62
|
Use EverCLI so the native plugin, MCP server, Cordis patch, and credentials stay in sync:
|
package/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
|
2
2
|
import {
|
|
3
3
|
assertConfigUsable,
|
|
4
4
|
createClient,
|
|
5
|
+
extractText,
|
|
5
6
|
redactError,
|
|
6
7
|
resolveConfig,
|
|
7
8
|
runInject,
|
|
@@ -64,13 +65,28 @@ export function installEverMeHooks(ctx, config = {}, dependencies = {}) {
|
|
|
64
65
|
|
|
65
66
|
ctx.on("session/event", (session, event) => {
|
|
66
67
|
if (event?.type !== "turn/end") return;
|
|
67
|
-
|
|
68
|
+
if (!validId(session?.id)) {
|
|
69
|
+
log.warn("[everme] conversion reason=invalid_session_id dropped=1");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const turn = event.data?.turn;
|
|
73
|
+
const messages = collectTurnMessages(session, turn, event.seq, log);
|
|
68
74
|
if (!messages.length) return;
|
|
75
|
+
if (isNativeSubagent(session) && !hasHumanInput(session, event.seq)) {
|
|
76
|
+
log.warn("[everme] conversion reason=subagent_without_user skipped=1");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
69
79
|
enqueue(pending, session, async () => {
|
|
70
80
|
await save(client, {
|
|
71
|
-
conversationId:
|
|
81
|
+
conversationId: session.id,
|
|
72
82
|
messages,
|
|
83
|
+
taskBatching: true,
|
|
73
84
|
flush: true,
|
|
85
|
+
// The host numbers turns from 1; the gate's whole-turn ordinals
|
|
86
|
+
// start at 0. Claiming the hook channel lets the gate advance its
|
|
87
|
+
// turn watermark for this live writer.
|
|
88
|
+
channel: "hook",
|
|
89
|
+
baseTurn: turn - 1,
|
|
74
90
|
}, log);
|
|
75
91
|
}, log);
|
|
76
92
|
});
|
|
@@ -82,7 +98,7 @@ export function installEverMeHooks(ctx, config = {}, dependencies = {}) {
|
|
|
82
98
|
return { enabled: true, pending };
|
|
83
99
|
}
|
|
84
100
|
|
|
85
|
-
export function collectTurnMessages(session, turn, endSeq = Number.POSITIVE_INFINITY) {
|
|
101
|
+
export function collectTurnMessages(session, turn, endSeq = Number.POSITIVE_INFINITY, log = { warn() {} }) {
|
|
86
102
|
if (!session || !Number.isSafeInteger(turn) || !Array.isArray(session.events)) return [];
|
|
87
103
|
const events = session.events;
|
|
88
104
|
let startIndex = -1;
|
|
@@ -99,60 +115,148 @@ export function collectTurnMessages(session, turn, endSeq = Number.POSITIVE_INFI
|
|
|
99
115
|
break;
|
|
100
116
|
}
|
|
101
117
|
}
|
|
102
|
-
if (startIndex < 0)
|
|
118
|
+
if (startIndex < 0) {
|
|
119
|
+
log.warn("[everme] conversion reason=missing_turn_start dropped=1");
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
103
122
|
|
|
104
123
|
const messages = [];
|
|
105
|
-
|
|
106
|
-
|
|
124
|
+
const rejected = new Map();
|
|
125
|
+
const reject = (reason) => rejected.set(reason, (rejected.get(reason) || 0) + 1);
|
|
126
|
+
const turnEvents = events.slice(startIndex + 1, endIndex);
|
|
127
|
+
const invalidResults = rejectedResultIDs(turnEvents);
|
|
128
|
+
const nativeSubagent = isNativeSubagent(session);
|
|
129
|
+
const lastSurface = turnEvents.findLast(event => ["user/message", "assistant/message", "tool/result"].includes(event.type));
|
|
130
|
+
const completed = turnEvents.at(-1)?.type === "turn/end" && turnEvents.at(-1).data?.reason?.kind === "completed";
|
|
131
|
+
for (const event of turnEvents) {
|
|
132
|
+
const converted = convertSessionEvent(event, reject, nativeSubagent);
|
|
133
|
+
if (converted?.role === "tool" && invalidResults.has(converted.toolCallId)) {
|
|
134
|
+
reject(invalidResults.get(converted.toolCallId));
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (converted?.role === "assistant") {
|
|
138
|
+
const original = event.data?.message?.content || [];
|
|
139
|
+
converted.turnComplete = completed && event === lastSurface && !event.data?.interrupted
|
|
140
|
+
&& !original.some(block => block.type === "tool-call")
|
|
141
|
+
&& converted.content.some(block => block.type === "text" && block.text.trim());
|
|
142
|
+
}
|
|
107
143
|
if (converted) messages.push(converted);
|
|
108
144
|
}
|
|
145
|
+
for (const [reason, count] of rejected) log.warn(`[everme] conversion reason=${reason} dropped=${count}`);
|
|
109
146
|
return messages;
|
|
110
147
|
}
|
|
111
148
|
|
|
112
|
-
function
|
|
149
|
+
function isNativeSubagent(session) {
|
|
150
|
+
if (session.header?.origin !== "subagent") return false;
|
|
151
|
+
const descriptor = session.events.find(event => event.type === "subagent/descriptor")?.data;
|
|
152
|
+
return descriptor?.version === 3 && ["one-shot", "continuable"].includes(descriptor.mode);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function hasHumanInput(session, endSeq) {
|
|
156
|
+
return session.events.some(event => event.seq <= endSeq && event.type === "user/message"
|
|
157
|
+
&& convertSessionEvent(event, () => {}, true)?.role === "user");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function convertSessionEvent(event, reject, nativeSubagent) {
|
|
113
161
|
if (event?.type === "user/message") {
|
|
114
162
|
const message = event.data;
|
|
115
|
-
if (message?.source?.kind !== "user")
|
|
116
|
-
|
|
163
|
+
if (message?.source?.kind !== "user") {
|
|
164
|
+
reject("non_user_source");
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
if (nativeSubagent && !("rpcId" in message.source)) {
|
|
168
|
+
reject("delegated_task");
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
const content = extractText(message.content).trim();
|
|
172
|
+
if (!content) reject("empty_message");
|
|
117
173
|
return content ? { role: "user", content, timestamp: event.time } : null;
|
|
118
174
|
}
|
|
119
175
|
|
|
120
176
|
if (event?.type === "assistant/message") {
|
|
121
|
-
const content = normalizeAssistantContent(event.data?.message?.content);
|
|
177
|
+
const content = normalizeAssistantContent(event.data?.message?.content, reject);
|
|
178
|
+
if (!content.length) reject("empty_message");
|
|
122
179
|
return content.length ? { role: "assistant", content, timestamp: event.time } : null;
|
|
123
180
|
}
|
|
124
181
|
|
|
125
182
|
if (event?.type === "tool/result") {
|
|
126
183
|
const block = event.data?.message?.content?.find((item) => item?.type === "tool-result");
|
|
127
|
-
if (!block?.toolCallId)
|
|
184
|
+
if (!validId(block?.toolCallId)) {
|
|
185
|
+
reject("invalid_tool_result_id");
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
if (!Array.isArray(block.content)) {
|
|
189
|
+
reject("invalid_tool_result_content");
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
128
192
|
return {
|
|
129
193
|
role: "tool",
|
|
130
|
-
toolCallId:
|
|
131
|
-
content: block.content
|
|
194
|
+
toolCallId: block.toolCallId,
|
|
195
|
+
content: block.content,
|
|
132
196
|
timestamp: event.time,
|
|
133
197
|
};
|
|
134
198
|
}
|
|
135
199
|
|
|
200
|
+
if (!["turn/start", "turn/end", "step/start", "step/end", "tool/call"].includes(event?.type)) reject("non_dialogue_event");
|
|
136
201
|
return null;
|
|
137
202
|
}
|
|
138
203
|
|
|
139
|
-
function normalizeAssistantContent(content) {
|
|
204
|
+
function normalizeAssistantContent(content, reject) {
|
|
140
205
|
const normalized = [];
|
|
141
206
|
for (const block of Array.isArray(content) ? content : []) {
|
|
142
207
|
if (block?.type === "text" && block.text) {
|
|
143
208
|
normalized.push({ type: "text", text: block.text });
|
|
144
|
-
} else if (block?.type === "tool-call"
|
|
209
|
+
} else if (block?.type === "tool-call") {
|
|
210
|
+
const reason = invalidCallReason(block);
|
|
211
|
+
if (reason) {
|
|
212
|
+
reject(reason);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
145
215
|
normalized.push({
|
|
146
216
|
type: "toolCall",
|
|
147
|
-
id:
|
|
148
|
-
name: block.name
|
|
217
|
+
id: block.id,
|
|
218
|
+
name: block.name,
|
|
149
219
|
arguments: block.arguments || "{}",
|
|
150
220
|
});
|
|
221
|
+
} else if (block?.type !== "text") {
|
|
222
|
+
reject("non_dialogue_content");
|
|
151
223
|
}
|
|
152
224
|
}
|
|
153
225
|
return normalized;
|
|
154
226
|
}
|
|
155
227
|
|
|
228
|
+
function validId(value) {
|
|
229
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function rejectedResultIDs(events) {
|
|
233
|
+
const counts = new Map();
|
|
234
|
+
const invalid = new Map();
|
|
235
|
+
for (const event of events) {
|
|
236
|
+
if (event.type !== "assistant/message") continue;
|
|
237
|
+
for (const block of event.data?.message?.content || []) {
|
|
238
|
+
if (block.type !== "tool-call" || !validId(block.id)) continue;
|
|
239
|
+
counts.set(block.id, (counts.get(block.id) || 0) + 1);
|
|
240
|
+
const reason = invalidCallReason(block);
|
|
241
|
+
if (reason) invalid.set(block.id, reason);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// Duplicate native IDs do not identify which call owns a result.
|
|
245
|
+
return new Map([...invalid].filter(([id]) => counts.get(id) === 1));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function invalidCallReason(block) {
|
|
249
|
+
if (!validId(block.id)) return "invalid_tool_call_id";
|
|
250
|
+
if (!validId(block.name)) return "invalid_tool_name";
|
|
251
|
+
if (typeof block.arguments !== "string") return "invalid_tool_arguments";
|
|
252
|
+
try {
|
|
253
|
+
JSON.parse(block.arguments || "{}");
|
|
254
|
+
} catch {
|
|
255
|
+
return "invalid_tool_arguments";
|
|
256
|
+
}
|
|
257
|
+
return "";
|
|
258
|
+
}
|
|
259
|
+
|
|
156
260
|
function humanPrompt(messages) {
|
|
157
261
|
return (Array.isArray(messages) ? messages : [])
|
|
158
262
|
.filter((message) => message?.source?.kind === "user")
|
|
@@ -186,5 +290,6 @@ function createLogger(ctx, override) {
|
|
|
186
290
|
}
|
|
187
291
|
|
|
188
292
|
function safeError(error) {
|
|
189
|
-
|
|
293
|
+
const text = redactError(error instanceof Error ? error.message : String(error)).replace(/\s+/g, " ").trim();
|
|
294
|
+
return text.length > 240 ? `${text.slice(0, 240)}…` : text;
|
|
190
295
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@everme/dsh",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Native EverMe lifecycle hooks for DeepSeek Harness.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"registry": "https://registry.npmjs.org"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@everme/agent-sdk": "^0.6.
|
|
46
|
+
"@everme/agent-sdk": "^0.6.5"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"@deepseek-ai/dsh-llm": ">=0.1.0-rc.6 <0.2.0"
|