@amaster.ai/dsh-storage 0.1.32 → 0.1.34
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 +21 -1
- package/lib/backends/database.js +79 -8
- package/lib/compaction.js +108 -0
- package/lib/index.js +75 -4
- package/lib/projector.js +5 -1
- package/lib/types/backends/database.d.ts +3 -1
- package/lib/types/compaction.d.ts +3 -0
- package/lib/types/projector.d.ts +1 -0
- package/lib/types/types.d.ts +4 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ The logical message id rides in `metadata.id`; message rows use a deterministic
|
|
|
55
55
|
|
|
56
56
|
For `user/message` rows, `content` remains the text projection, while `metadata.message` retains the original user message as received, including `source` and ordered content blocks. Image/file blocks keep whatever attachment reference, path, URL, or inline data the incoming message actually contains; this mirror does not fetch or rewrite attachments. Re-projection writes the same raw message into the existing row.
|
|
57
57
|
|
|
58
|
-
The `tool_calls` array retains dsh's tool-call blocks (`id`, `name`, `arguments`); the matching entry gains the structured `result` block and, on failure, an `error` identity.
|
|
58
|
+
The `tool_calls` array retains dsh's tool-call blocks (`id`, `name`, `arguments`); the matching entry gains the structured `result` block and, on failure, an `error` identity. Live call IDs are cached, and restored calls can resolve their message ID from the model surface: both paths read one message primary key. Older calls are searched in keyset pages of at most 100 ID/tool-call candidates, matched in the plugin and discarded before fetching the next page. Tool-result lookup SQL does not search JSON, join tables, or nest SELECT statements. Duplicate results update the same row and do not increment message counts. Existing historical `type=tool` rows are left intact.
|
|
59
59
|
|
|
60
60
|
When an assistant message contains dsh `reasoning` blocks, `thoughts` is an array of `{ content }` entries in block order. If the adapter supplies a matching signature in the message's `source.replayState.blocks`, that entry also carries `thoughtSignature`. The full model `source`, including opaque `replayState`, is preserved in row metadata for lossless provider replay. The backend's `readMessages()` also accepts older `{ description }` entries and JSON string thoughts, returning them as `{ content }`. `subject` and a per-thought timestamp are not invented when the upstream message does not supply them.
|
|
61
61
|
|
|
@@ -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.
|
package/lib/backends/database.js
CHANGED
|
@@ -143,18 +143,89 @@ export class DatabaseBackend {
|
|
|
143
143
|
});
|
|
144
144
|
}
|
|
145
145
|
async readMessages(sessionId, messageId) {
|
|
146
|
+
return this.readRows({
|
|
147
|
+
sessionId,
|
|
148
|
+
deletedAt: null,
|
|
149
|
+
id: pk(`message ${sessionId}\0${messageId}`),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
async readToolCallMessages(sessionId, callId) {
|
|
153
|
+
if (!this.prisma)
|
|
154
|
+
return [];
|
|
155
|
+
const provider = this.config.provider;
|
|
156
|
+
const sqlserver = provider === 'sqlserver';
|
|
157
|
+
const sortTime = provider === 'mysql'
|
|
158
|
+
? "DATE_FORMAT(created_at, '%Y-%m-%d %H:%i:%s.%f')"
|
|
159
|
+
: provider === 'postgresql'
|
|
160
|
+
? "to_char(created_at, 'YYYY-MM-DD HH24:MI:SS.US')"
|
|
161
|
+
: sqlserver
|
|
162
|
+
? 'CONVERT(varchar(33), created_at, 126)'
|
|
163
|
+
: 'CAST(created_at AS TEXT)';
|
|
164
|
+
let cursor;
|
|
165
|
+
for (;;) {
|
|
166
|
+
const params = provider === 'mysql' || provider === 'sqlite' ? [sessionId, sessionId] : [sessionId];
|
|
167
|
+
let before = '';
|
|
168
|
+
if (cursor) {
|
|
169
|
+
if (cursor.time === null) {
|
|
170
|
+
before =
|
|
171
|
+
provider === 'postgresql'
|
|
172
|
+
? 'AND (created_at IS NOT NULL OR (created_at IS NULL AND id < ?))'
|
|
173
|
+
: 'AND created_at IS NULL AND id < ?';
|
|
174
|
+
params.push(cursor.id);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
before = `AND (created_at < ? OR (created_at = ? AND id < ?)${provider === 'postgresql' ? '' : ' OR created_at IS NULL'})`;
|
|
178
|
+
params.push(cursor.time, cursor.time, cursor.id);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
let sql = `SELECT ${sqlserver ? 'TOP 100 ' : ''}id, tool_calls, ${sortTime} AS sort_time
|
|
182
|
+
FROM ai_messages WHERE session_id = ?
|
|
183
|
+
${provider === 'sqlite' ? 'AND session_id COLLATE BINARY = ?' : ''}
|
|
184
|
+
${provider === 'mysql' ? 'AND BINARY session_id = BINARY ?' : ''}
|
|
185
|
+
AND deleted_at IS NULL AND history_id IS NULL AND type = 'model' AND tool_calls IS NOT NULL
|
|
186
|
+
${sqlserver
|
|
187
|
+
? `AND session_id COLLATE Latin1_General_100_BIN2 = @P1
|
|
188
|
+
AND DATALENGTH(CAST(session_id AS nvarchar(max))) = DATALENGTH(CAST(@P1 AS nvarchar(max)))`
|
|
189
|
+
: ''}
|
|
190
|
+
${before} ORDER BY created_at DESC, id DESC ${sqlserver ? '' : 'LIMIT 100'}`;
|
|
191
|
+
if (sqlserver || provider === 'postgresql') {
|
|
192
|
+
let index = 0;
|
|
193
|
+
sql = sql.replaceAll('?', () => `${sqlserver ? '@P' : '$'}${++index}`);
|
|
194
|
+
}
|
|
195
|
+
const page = await this.prisma.$queryRawUnsafe(sql, ...params);
|
|
196
|
+
for (const row of page) {
|
|
197
|
+
let calls = row.tool_calls;
|
|
198
|
+
if (typeof calls === 'string') {
|
|
199
|
+
try {
|
|
200
|
+
calls = JSON.parse(calls);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (Array.isArray(calls) && calls.some((call) => call?.id === callId)) {
|
|
207
|
+
return this.readRows({
|
|
208
|
+
sessionId,
|
|
209
|
+
deletedAt: null,
|
|
210
|
+
historyId: null,
|
|
211
|
+
type: 'model',
|
|
212
|
+
id: row.id,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (page.length < 100)
|
|
217
|
+
return [];
|
|
218
|
+
const last = page.at(-1);
|
|
219
|
+
cursor = { id: last.id, time: last.sort_time };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async readRows(where) {
|
|
146
223
|
if (!this.prisma)
|
|
147
224
|
return [];
|
|
148
|
-
// ponytail: tool-call lookup scans one session; add an indexed call lookup if that becomes slow.
|
|
149
|
-
// Scan only the active segment for tool results; exact-ID reads still find
|
|
150
|
-
// archived rows so redelivery does not count the same message twice.
|
|
151
225
|
const rows = await this.prisma.aiMessage.findMany({
|
|
152
|
-
where
|
|
153
|
-
sessionId,
|
|
154
|
-
deletedAt: null,
|
|
155
|
-
...(messageId ? { id: pk(`message ${sessionId}\0${messageId}`) } : { historyId: null }),
|
|
156
|
-
},
|
|
226
|
+
where,
|
|
157
227
|
orderBy: { createdAt: 'asc' },
|
|
228
|
+
take: 1,
|
|
158
229
|
});
|
|
159
230
|
const json = (value) => (typeof value === 'string' ? JSON.parse(value) : value);
|
|
160
231
|
return rows.map((row) => {
|
|
@@ -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,7 +19,8 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import Schema from '@deepseek-ai/schemastery';
|
|
21
21
|
import { DatabaseBackend } from './backends/database.js';
|
|
22
|
-
import {
|
|
22
|
+
import { compressionSnapshot } from './compaction.js';
|
|
23
|
+
import { mergeToolResult, projectEvent, toolResultCallId, usageSampleOf } from './projector.js';
|
|
23
24
|
export const name = 'dsh-storage';
|
|
24
25
|
// Event-tap only: no hard service dependency. Add 'sessionPersistence' to
|
|
25
26
|
// `inject` if you want fail-fast when persistence is not composed.
|
|
@@ -62,6 +63,54 @@ export function apply(ctx, config) {
|
|
|
62
63
|
}),
|
|
63
64
|
];
|
|
64
65
|
const sessions = new Map();
|
|
66
|
+
const toolCallMessages = new Map();
|
|
67
|
+
const TOOL_CALL_CACHE_LIMIT = 1000;
|
|
68
|
+
function rememberToolCalls(sessionId, row) {
|
|
69
|
+
if (row?.type !== 'model' || !Array.isArray(row.toolCalls))
|
|
70
|
+
return;
|
|
71
|
+
for (const call of row.toolCalls) {
|
|
72
|
+
if (typeof call?.id !== 'string')
|
|
73
|
+
continue;
|
|
74
|
+
const calls = toolCallMessages.get(sessionId) ?? new Map();
|
|
75
|
+
// Bound each session's cache; Map iteration evicts the oldest insertion first.
|
|
76
|
+
if (!calls.has(call.id) && calls.size >= TOOL_CALL_CACHE_LIMIT) {
|
|
77
|
+
const oldest = calls.keys().next().value;
|
|
78
|
+
if (oldest !== undefined)
|
|
79
|
+
calls.delete(oldest);
|
|
80
|
+
}
|
|
81
|
+
calls.set(call.id, row.id);
|
|
82
|
+
toolCallMessages.set(sessionId, calls);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function readToolResultRows(session, sessionId, event) {
|
|
86
|
+
const callId = toolResultCallId(event);
|
|
87
|
+
if (!callId)
|
|
88
|
+
return [];
|
|
89
|
+
let messageId = toolCallMessages.get(sessionId)?.get(callId);
|
|
90
|
+
if (!messageId && typeof session?.deriveMessages === 'function') {
|
|
91
|
+
const messages = session.deriveMessages();
|
|
92
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
93
|
+
const message = messages[index];
|
|
94
|
+
if (message.role === 'assistant' &&
|
|
95
|
+
typeof message.id === 'string' &&
|
|
96
|
+
Array.isArray(message.content) &&
|
|
97
|
+
message.content.some((block) => block?.type === 'tool-call' && block.id === callId)) {
|
|
98
|
+
messageId = message.id;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (messageId) {
|
|
104
|
+
const rows = await backends[0].readMessages(sessionId, messageId);
|
|
105
|
+
if (rows.some((row) => Array.isArray(row.toolCalls) && row.toolCalls.some((call) => call?.id === callId)))
|
|
106
|
+
return rows;
|
|
107
|
+
}
|
|
108
|
+
// Older calls absent from the live surface are searched in bounded database pages.
|
|
109
|
+
const rows = await backends[0].readToolCallMessages(sessionId, callId);
|
|
110
|
+
for (const row of rows)
|
|
111
|
+
rememberToolCalls(sessionId, row);
|
|
112
|
+
return rows;
|
|
113
|
+
}
|
|
65
114
|
// Row writes are fire-and-forget during the run, but tracked so the
|
|
66
115
|
// session/flush checkpoint and shutdown can drain them — a one-shot
|
|
67
116
|
// (headless) run would otherwise lose the tail events to process exit.
|
|
@@ -132,6 +181,7 @@ export function apply(ctx, config) {
|
|
|
132
181
|
started = track(guard(async (backend) => backend.init?.()));
|
|
133
182
|
return async () => {
|
|
134
183
|
sessions.clear();
|
|
184
|
+
toolCallMessages.clear();
|
|
135
185
|
await Promise.all([...pending]);
|
|
136
186
|
await guard(async (backend) => backend.close?.());
|
|
137
187
|
};
|
|
@@ -143,6 +193,7 @@ export function apply(ctx, config) {
|
|
|
143
193
|
const archived = (chains.get(sessionId) ?? started).then(async () => {
|
|
144
194
|
const id = await backends[0].archiveSession(sessionId);
|
|
145
195
|
sessions.delete(sessionId);
|
|
196
|
+
toolCallMessages.delete(sessionId);
|
|
146
197
|
return id;
|
|
147
198
|
});
|
|
148
199
|
const settled = archived.then(() => { }, () => { });
|
|
@@ -154,16 +205,34 @@ export function apply(ctx, config) {
|
|
|
154
205
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
155
206
|
ctx.on('session/event', (session, event) => {
|
|
156
207
|
const sessionId = session?.id ?? 'unknown';
|
|
208
|
+
let snapshot;
|
|
209
|
+
try {
|
|
210
|
+
// Capture the committed surface now; queued writes must not observe later steps.
|
|
211
|
+
snapshot = compressionSnapshot(session, event);
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
console.error('[dsh-storage] compaction snapshot error:', error);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
157
217
|
enqueue(sessionId, async () => {
|
|
218
|
+
if (snapshot) {
|
|
219
|
+
await fanout((backend) => backend.upsertMessage(snapshot));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
158
222
|
let accum = sessions.get(sessionId);
|
|
159
223
|
if (!accum) {
|
|
160
224
|
accum = await seedAccum(sessionId);
|
|
161
225
|
sessions.set(sessionId, accum);
|
|
162
226
|
}
|
|
163
227
|
let row = projectEvent(session, event, sessionId);
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
228
|
+
if (row && session.header?.createdAt !== undefined) {
|
|
229
|
+
row.metadata = { ...row.metadata, sessionCreatedAt: session.header.createdAt };
|
|
230
|
+
}
|
|
231
|
+
const storedRows = event?.type === 'tool/result'
|
|
232
|
+
? await readToolResultRows(session, sessionId, event)
|
|
233
|
+
: row
|
|
234
|
+
? await backends[0].readMessages(sessionId, row.id)
|
|
235
|
+
: [];
|
|
167
236
|
if (event?.type === 'tool/result')
|
|
168
237
|
row = mergeToolResult(event, storedRows);
|
|
169
238
|
const existing = row && storedRows.find((stored) => stored.id === row?.id);
|
|
@@ -191,6 +260,7 @@ export function apply(ctx, config) {
|
|
|
191
260
|
accum.lastMessageAt = row.createdAt;
|
|
192
261
|
}
|
|
193
262
|
await fanout((backend) => backend.upsertMessage(row));
|
|
263
|
+
rememberToolCalls(sessionId, row);
|
|
194
264
|
}
|
|
195
265
|
// Replacement usage accounting: the latest sample for a (turn, step)
|
|
196
266
|
// supersedes the previous one — fold only the delta, so the
|
|
@@ -246,6 +316,7 @@ export function apply(ctx, config) {
|
|
|
246
316
|
// the dispose was meant to drop).
|
|
247
317
|
enqueue(sessionId, async () => {
|
|
248
318
|
sessions.delete(sessionId);
|
|
319
|
+
toolCallMessages.delete(sessionId);
|
|
249
320
|
});
|
|
250
321
|
// Delete the chain entry only once it has settled AND is still the same
|
|
251
322
|
// entry — deleting earlier would let a recreated session race the
|
package/lib/projector.js
CHANGED
|
@@ -106,10 +106,14 @@ export function projectEvent(_session, event, sessionId) {
|
|
|
106
106
|
return null;
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
|
+
export function toolResultCallId(event) {
|
|
110
|
+
const callId = event?.data?.message?.source?.callId ?? event?.data?.message?.content?.[0]?.toolCallId;
|
|
111
|
+
return typeof callId === 'string' ? callId : undefined;
|
|
112
|
+
}
|
|
109
113
|
/** Update the persisted assistant call identified by a tool result. */
|
|
110
114
|
export function mergeToolResult(event, rows) {
|
|
111
115
|
const result = event?.data?.message?.content?.[0] ?? null;
|
|
112
|
-
const callId = event
|
|
116
|
+
const callId = toolResultCallId(event);
|
|
113
117
|
if (!callId)
|
|
114
118
|
return null;
|
|
115
119
|
const model = [...rows]
|
|
@@ -31,7 +31,9 @@ export declare class DatabaseBackend implements StorageBackend {
|
|
|
31
31
|
private jsonField;
|
|
32
32
|
private toDbData;
|
|
33
33
|
upsertMessage(row: MessageRow): Promise<void>;
|
|
34
|
-
readMessages(sessionId: string, messageId
|
|
34
|
+
readMessages(sessionId: string, messageId: string): Promise<MessageRow[]>;
|
|
35
|
+
readToolCallMessages(sessionId: string, callId: string): Promise<MessageRow[]>;
|
|
36
|
+
private readRows;
|
|
35
37
|
readSession(sessionId: string): Promise<SessionRow | null>;
|
|
36
38
|
upsertSession(row: SessionRow): Promise<void>;
|
|
37
39
|
archiveSession(sessionId: string): Promise<string | null>;
|
package/lib/types/projector.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ import type { MessageRow } from './types.js';
|
|
|
22
22
|
* turn/step lifecycle, approvals, ...).
|
|
23
23
|
*/
|
|
24
24
|
export declare function projectEvent(_session: any, event: any, sessionId: string): MessageRow | null;
|
|
25
|
+
export declare function toolResultCallId(event: any): string | undefined;
|
|
25
26
|
/** Update the persisted assistant call identified by a tool result. */
|
|
26
27
|
export declare function mergeToolResult(event: any, rows: MessageRow[]): MessageRow | null;
|
|
27
28
|
export interface UsageSample {
|
package/lib/types/types.d.ts
CHANGED
|
@@ -43,8 +43,10 @@ export interface StorageBackend {
|
|
|
43
43
|
init?(): Promise<void>;
|
|
44
44
|
/** Read the stored session row, for seeding the live rollup on resume. */
|
|
45
45
|
readSession?(sessionId: string): Promise<SessionRow | null>;
|
|
46
|
-
/** Read persisted
|
|
47
|
-
readMessages(sessionId: string, messageId
|
|
46
|
+
/** Read one persisted logical message, including archived rows for redelivery. */
|
|
47
|
+
readMessages(sessionId: string, messageId: string): Promise<MessageRow[]>;
|
|
48
|
+
/** Find at most one active model message containing this exact tool call. */
|
|
49
|
+
readToolCallMessages(sessionId: string, callId: string): Promise<MessageRow[]>;
|
|
48
50
|
upsertMessage(row: MessageRow): Promise<void>;
|
|
49
51
|
upsertSession(row: SessionRow): Promise<void>;
|
|
50
52
|
/** Move active messages into a queryable history row; null when already empty. */
|
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.34",
|
|
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",
|