@amaster.ai/dsh-storage 0.1.32 → 0.1.33
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 +20 -0
- package/lib/compaction.js +108 -0
- package/lib/index.js +17 -0
- package/lib/types/compaction.d.ts +3 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -63,6 +63,26 @@ SQL Server note: Prisma's sqlserver connector has no `Json` type, so its variant
|
|
|
63
63
|
|
|
64
64
|
Mirroring is a no-throw seam: backend errors are logged with a `[dsh-storage]` prefix and never escape into the agent loop.
|
|
65
65
|
|
|
66
|
+
Successful native `compaction/end` events also write a `utility_compressor` message. Its
|
|
67
|
+
`content` is the source-compatible `Content[]` JSON array of the complete surviving
|
|
68
|
+
conversation: summary plus retained tail, with tool calls/results paired by ID. System
|
|
69
|
+
prompts are excluded. The surface is captured at the event, before asynchronous database
|
|
70
|
+
writes; failed/canceled compactions leave the previous snapshot intact. Snapshot rows do
|
|
71
|
+
not increment the visible message count. Adjacent messages with the same role merge their
|
|
72
|
+
ordered parts, since the legacy reader would otherwise skip the retained tail.
|
|
73
|
+
|
|
74
|
+
Consumers can resume from the latest active compression snapshot plus later message rows.
|
|
75
|
+
Snapshots and newly mirrored rows include `metadata.sessionCreatedAt` and `metadata.seq`
|
|
76
|
+
to distinguish resumed session generations and messages in the same database second.
|
|
77
|
+
Older snapshots without this metadata retain their creation-time boundary. The mirror
|
|
78
|
+
does not fetch attachments or invent usable paths/URLs from opaque attachment IDs.
|
|
79
|
+
Attachments, developer updates, extension blocks and non-object tool arguments are
|
|
80
|
+
preserved as labelled JSON data in text parts when legacy `Content[]` cannot represent
|
|
81
|
+
them natively. Such snapshots preserve reference information, not attachment bytes or a
|
|
82
|
+
complete native session/tool registry. Attachment bytes still need their existing durable
|
|
83
|
+
storage. An unchanged legacy reader remains timestamp-based; it must consume the sequence
|
|
84
|
+
metadata to distinguish later messages in the same database second.
|
|
85
|
+
|
|
66
86
|
## Archive a session
|
|
67
87
|
|
|
68
88
|
When handling a session-clear request, call `await ctx.get('dshStorage').archiveSession(sessionId)` after stopping the live turn. It waits for queued mirror writes, then transactionally assigns all current, non-deleted `ai_messages` rows (`history_id IS NULL`) to a non-deleted `ai_chat_histories` row. The returned history ID can be used to query those rows directly from the database; a second call with no active messages returns `null`. A database failure rejects this explicit operation instead of reporting a successful clear.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
function recordedData(block) {
|
|
2
|
+
return { text: `[Recorded DSH ${block.type} data]\n${JSON.stringify(block)}` };
|
|
3
|
+
}
|
|
4
|
+
/** Legacy Content[] checkpoints remain readable by databaseChatStorage. */
|
|
5
|
+
export function compressionSnapshot(session, event) {
|
|
6
|
+
if (event?.type !== 'compaction/end' || event.data?.error !== undefined)
|
|
7
|
+
return null;
|
|
8
|
+
const calls = new Map();
|
|
9
|
+
const messages = session.deriveMessages().flatMap((message) => {
|
|
10
|
+
if (message.role === 'system')
|
|
11
|
+
return [];
|
|
12
|
+
if (message.role === 'tool') {
|
|
13
|
+
const id = message.toolCallId ?? message.source?.callId;
|
|
14
|
+
const name = calls.get(id);
|
|
15
|
+
if (!name)
|
|
16
|
+
return [
|
|
17
|
+
{
|
|
18
|
+
role: 'user',
|
|
19
|
+
parts: [
|
|
20
|
+
recordedData({
|
|
21
|
+
type: 'tool-result',
|
|
22
|
+
toolCallId: id,
|
|
23
|
+
content: message.content,
|
|
24
|
+
isError: Boolean(message.isError),
|
|
25
|
+
}),
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
return [
|
|
30
|
+
{
|
|
31
|
+
role: 'user',
|
|
32
|
+
parts: [
|
|
33
|
+
{
|
|
34
|
+
functionResponse: {
|
|
35
|
+
id,
|
|
36
|
+
name,
|
|
37
|
+
response: { output: message.content, isError: Boolean(message.isError) },
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
const parts = message.content.map((block, index) => {
|
|
45
|
+
if (message.role === 'developer')
|
|
46
|
+
return recordedData(block);
|
|
47
|
+
switch (block.type) {
|
|
48
|
+
case 'text':
|
|
49
|
+
return { text: block.text };
|
|
50
|
+
case 'reasoning': {
|
|
51
|
+
const replay = message.source?.replayState?.blocks?.[index];
|
|
52
|
+
const signature = replay?.signature ?? replay?.thoughtSignature;
|
|
53
|
+
return {
|
|
54
|
+
text: block.text,
|
|
55
|
+
thought: true,
|
|
56
|
+
...(signature ? { thoughtSignature: signature } : {}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
case 'tool-call': {
|
|
60
|
+
calls.delete(block.id);
|
|
61
|
+
let args;
|
|
62
|
+
try {
|
|
63
|
+
args = block.arguments ? JSON.parse(block.arguments) : {};
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return recordedData(block);
|
|
67
|
+
}
|
|
68
|
+
if (!args || typeof args !== 'object' || Array.isArray(args))
|
|
69
|
+
return recordedData(block);
|
|
70
|
+
calls.set(block.id, block.name);
|
|
71
|
+
return {
|
|
72
|
+
functionCall: { id: block.id, name: block.name, args },
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
default:
|
|
76
|
+
return recordedData(block);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
return [{ role: message.role === 'assistant' ? 'model' : 'user', parts }];
|
|
80
|
+
});
|
|
81
|
+
const content = [];
|
|
82
|
+
for (const message of messages) {
|
|
83
|
+
const previous = content.at(-1);
|
|
84
|
+
// Legacy recovery discards adjacent equal roles instead of merging them.
|
|
85
|
+
if (previous && previous.role === message.role)
|
|
86
|
+
previous.parts.push(...message.parts);
|
|
87
|
+
else
|
|
88
|
+
content.push(message);
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
id: event.data.compactionId,
|
|
92
|
+
sessionId: session.id,
|
|
93
|
+
historyId: null,
|
|
94
|
+
agentId: 'main',
|
|
95
|
+
type: 'utility_compressor',
|
|
96
|
+
content: JSON.stringify(content),
|
|
97
|
+
model: 'agent-history-provider-summarizer',
|
|
98
|
+
createdAt: new Date(event.time),
|
|
99
|
+
metadata: {
|
|
100
|
+
id: event.data.compactionId,
|
|
101
|
+
event: event.type,
|
|
102
|
+
seq: event.seq,
|
|
103
|
+
sessionCreatedAt: session.header?.createdAt,
|
|
104
|
+
sessionId: session.id,
|
|
105
|
+
newHistoryLength: content.length,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import Schema from '@deepseek-ai/schemastery';
|
|
21
21
|
import { DatabaseBackend } from './backends/database.js';
|
|
22
|
+
import { compressionSnapshot } from './compaction.js';
|
|
22
23
|
import { mergeToolResult, projectEvent, usageSampleOf } from './projector.js';
|
|
23
24
|
export const name = 'dsh-storage';
|
|
24
25
|
// Event-tap only: no hard service dependency. Add 'sessionPersistence' to
|
|
@@ -154,13 +155,29 @@ export function apply(ctx, config) {
|
|
|
154
155
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
155
156
|
ctx.on('session/event', (session, event) => {
|
|
156
157
|
const sessionId = session?.id ?? 'unknown';
|
|
158
|
+
let snapshot;
|
|
159
|
+
try {
|
|
160
|
+
// Capture the committed surface now; queued writes must not observe later steps.
|
|
161
|
+
snapshot = compressionSnapshot(session, event);
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
console.error('[dsh-storage] compaction snapshot error:', error);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
157
167
|
enqueue(sessionId, async () => {
|
|
168
|
+
if (snapshot) {
|
|
169
|
+
await fanout((backend) => backend.upsertMessage(snapshot));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
158
172
|
let accum = sessions.get(sessionId);
|
|
159
173
|
if (!accum) {
|
|
160
174
|
accum = await seedAccum(sessionId);
|
|
161
175
|
sessions.set(sessionId, accum);
|
|
162
176
|
}
|
|
163
177
|
let row = projectEvent(session, event, sessionId);
|
|
178
|
+
if (row && session.header?.createdAt !== undefined) {
|
|
179
|
+
row.metadata = { ...row.metadata, sessionCreatedAt: session.header.createdAt };
|
|
180
|
+
}
|
|
164
181
|
const storedRows = row || event?.type === 'tool/result'
|
|
165
182
|
? await backends[0].readMessages(sessionId, row?.id)
|
|
166
183
|
: [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amaster.ai/dsh-storage",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.33",
|
|
4
4
|
"description": "Session storage mirror for DeepSeek Harness: projects the session event stream into MySQL/PostgreSQL/SQLite/SQL Server (ai_messages / ai_chat_histories) without replacing dsh's local persistence.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@deepseek-ai/cordis": "4.0.5",
|
|
66
|
-
"@deepseek-ai/dsh-session": "0.2.1-rc.
|
|
66
|
+
"@deepseek-ai/dsh-session": "0.2.1-rc.4",
|
|
67
67
|
"@prisma/adapter-libsql": "^7.9.1",
|
|
68
68
|
"@prisma/adapter-mariadb": "^7.9.1",
|
|
69
69
|
"@prisma/adapter-mssql": "^7.9.1",
|