@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 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
  : [];
@@ -0,0 +1,3 @@
1
+ import type { MessageRow } from './types.js';
2
+ /** Legacy Content[] checkpoints remain readable by databaseChatStorage. */
3
+ export declare function compressionSnapshot(session: any, event: any): MessageRow | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amaster.ai/dsh-storage",
3
- "version": "0.1.32",
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.3",
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",