@hermespilot/link 0.3.5 → 0.3.7
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 +1 -1
- package/dist/{chunk-ZQO7TU7G.js → chunk-TZVQZFWU.js} +1376 -823
- package/dist/cli/index.js +1 -1
- package/dist/http/app.d.ts +97 -78
- package/dist/http/app.js +1 -1
- package/package.json +6 -4
- package/scripts/check-node-version.mjs +7 -7
|
@@ -9,8 +9,21 @@ import { randomUUID as randomUUID7 } from "crypto";
|
|
|
9
9
|
// src/database/link-database.ts
|
|
10
10
|
import { mkdir } from "fs/promises";
|
|
11
11
|
import { randomUUID } from "crypto";
|
|
12
|
-
import { createRequire } from "module";
|
|
13
12
|
import path from "path";
|
|
13
|
+
|
|
14
|
+
// src/database/sqlite.ts
|
|
15
|
+
import Database from "better-sqlite3";
|
|
16
|
+
function openSqliteDatabase(filePath, options = {}) {
|
|
17
|
+
return new Database(filePath, {
|
|
18
|
+
...options.readonly === void 0 ? {} : {
|
|
19
|
+
readonly: options.readonly,
|
|
20
|
+
fileMustExist: options.readonly
|
|
21
|
+
},
|
|
22
|
+
...options.timeout === void 0 ? {} : { timeout: options.timeout }
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/database/link-database.ts
|
|
14
27
|
var MIGRATIONS = [
|
|
15
28
|
{
|
|
16
29
|
version: 1,
|
|
@@ -136,7 +149,6 @@ var MIGRATIONS = [
|
|
|
136
149
|
`
|
|
137
150
|
}
|
|
138
151
|
];
|
|
139
|
-
var nodeRequire = createRequire(import.meta.url);
|
|
140
152
|
async function migrateLinkDatabase(paths) {
|
|
141
153
|
await mkdir(path.dirname(paths.databaseFile), { recursive: true, mode: 448 });
|
|
142
154
|
const db = openDatabase(paths);
|
|
@@ -210,6 +222,92 @@ async function upsertConversationStats(paths, record) {
|
|
|
210
222
|
db.close();
|
|
211
223
|
}
|
|
212
224
|
}
|
|
225
|
+
async function listConversationStatsPage(paths, input) {
|
|
226
|
+
await migrateLinkDatabase(paths);
|
|
227
|
+
const rawLimit = Number.isFinite(input.limit) ? Math.trunc(input.limit) : 25;
|
|
228
|
+
const limit = Math.max(1, Math.min(100, rawLimit));
|
|
229
|
+
const db = openDatabase(paths);
|
|
230
|
+
try {
|
|
231
|
+
const conditions = ["status = ?"];
|
|
232
|
+
const params = ["active"];
|
|
233
|
+
if (input.cursor) {
|
|
234
|
+
conditions.push(`(
|
|
235
|
+
updated_at < ?
|
|
236
|
+
OR (updated_at = ? AND conversation_id < ?)
|
|
237
|
+
)`);
|
|
238
|
+
params.push(
|
|
239
|
+
input.cursor.updatedAt,
|
|
240
|
+
input.cursor.updatedAt,
|
|
241
|
+
input.cursor.conversationId
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
const rows = db.prepare(`
|
|
245
|
+
SELECT conversation_id, updated_at
|
|
246
|
+
FROM conversation_stats
|
|
247
|
+
WHERE ${conditions.join(" AND ")}
|
|
248
|
+
ORDER BY updated_at DESC, conversation_id DESC
|
|
249
|
+
LIMIT ?
|
|
250
|
+
`).all(...params, limit + 1);
|
|
251
|
+
const records = rows.slice(0, limit).map((row) => ({
|
|
252
|
+
conversationId: readString(row, "conversation_id") ?? "",
|
|
253
|
+
updatedAt: readString(row, "updated_at") ?? ""
|
|
254
|
+
})).filter((row) => row.conversationId && row.updatedAt);
|
|
255
|
+
return {
|
|
256
|
+
records,
|
|
257
|
+
hasMore: rows.length > limit
|
|
258
|
+
};
|
|
259
|
+
} finally {
|
|
260
|
+
db.close();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
async function searchConversationStatsPage(paths, input) {
|
|
264
|
+
await migrateLinkDatabase(paths);
|
|
265
|
+
const rawLimit = Number.isFinite(input.limit) ? Math.trunc(input.limit) : 25;
|
|
266
|
+
const limit = Math.max(1, Math.min(100, rawLimit));
|
|
267
|
+
const query = input.query.trim();
|
|
268
|
+
if (!query) {
|
|
269
|
+
return listConversationStatsPage(paths, {
|
|
270
|
+
limit,
|
|
271
|
+
cursor: input.cursor
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
const db = openDatabase(paths);
|
|
275
|
+
try {
|
|
276
|
+
const conditions = ["status = ?", "LOWER(title) LIKE ? ESCAPE '\\'"];
|
|
277
|
+
const params = [
|
|
278
|
+
"active",
|
|
279
|
+
`%${escapeSqlLike(query.toLowerCase())}%`
|
|
280
|
+
];
|
|
281
|
+
if (input.cursor) {
|
|
282
|
+
conditions.push(`(
|
|
283
|
+
updated_at < ?
|
|
284
|
+
OR (updated_at = ? AND conversation_id < ?)
|
|
285
|
+
)`);
|
|
286
|
+
params.push(
|
|
287
|
+
input.cursor.updatedAt,
|
|
288
|
+
input.cursor.updatedAt,
|
|
289
|
+
input.cursor.conversationId
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
const rows = db.prepare(`
|
|
293
|
+
SELECT conversation_id, updated_at
|
|
294
|
+
FROM conversation_stats
|
|
295
|
+
WHERE ${conditions.join(" AND ")}
|
|
296
|
+
ORDER BY updated_at DESC, conversation_id DESC
|
|
297
|
+
LIMIT ?
|
|
298
|
+
`).all(...params, limit + 1);
|
|
299
|
+
const records = rows.slice(0, limit).map((row) => ({
|
|
300
|
+
conversationId: readString(row, "conversation_id") ?? "",
|
|
301
|
+
updatedAt: readString(row, "updated_at") ?? ""
|
|
302
|
+
})).filter((row) => row.conversationId && row.updatedAt);
|
|
303
|
+
return {
|
|
304
|
+
records,
|
|
305
|
+
hasMore: rows.length > limit
|
|
306
|
+
};
|
|
307
|
+
} finally {
|
|
308
|
+
db.close();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
213
311
|
async function replaceRunUsageFactsForConversation(paths, conversationId, records) {
|
|
214
312
|
await migrateLinkDatabase(paths);
|
|
215
313
|
const db = openDatabase(paths);
|
|
@@ -618,8 +716,7 @@ async function deleteConversationStatsForProfile(paths, input) {
|
|
|
618
716
|
}
|
|
619
717
|
}
|
|
620
718
|
function openDatabase(paths) {
|
|
621
|
-
const
|
|
622
|
-
const db = new DatabaseSync(paths.databaseFile, {
|
|
719
|
+
const db = openSqliteDatabase(paths.databaseFile, {
|
|
623
720
|
timeout: 5e3
|
|
624
721
|
});
|
|
625
722
|
db.exec(`
|
|
@@ -826,6 +923,9 @@ function readString(row, key) {
|
|
|
826
923
|
const value = row?.[key];
|
|
827
924
|
return typeof value === "string" && value.trim() ? value : void 0;
|
|
828
925
|
}
|
|
926
|
+
function escapeSqlLike(value) {
|
|
927
|
+
return value.replace(/[\\%_]/gu, (match) => `\\${match}`);
|
|
928
|
+
}
|
|
829
929
|
function statisticsWhereClause(filter) {
|
|
830
930
|
const profileUid = filter.profileUid?.trim();
|
|
831
931
|
const profileName = filter.profileName?.trim();
|
|
@@ -3724,7 +3824,7 @@ import os2 from "os";
|
|
|
3724
3824
|
import path5 from "path";
|
|
3725
3825
|
|
|
3726
3826
|
// src/constants.ts
|
|
3727
|
-
var LINK_VERSION = "0.3.
|
|
3827
|
+
var LINK_VERSION = "0.3.7";
|
|
3728
3828
|
var LINK_COMMAND = "hermeslink";
|
|
3729
3829
|
var LINK_DEFAULT_PORT = 52379;
|
|
3730
3830
|
var LINK_RUNTIME_DIR_NAME = ".hermeslink";
|
|
@@ -4859,6 +4959,9 @@ function toRecord3(value) {
|
|
|
4859
4959
|
}
|
|
4860
4960
|
|
|
4861
4961
|
// src/conversations/statistics.ts
|
|
4962
|
+
var ESTIMATED_CONTEXT_CHARS_PER_TOKEN = 4;
|
|
4963
|
+
var ESTIMATED_CONTEXT_BASE_OVERHEAD_TOKENS = 256;
|
|
4964
|
+
var ESTIMATED_CONTEXT_PER_MESSAGE_OVERHEAD_TOKENS = 8;
|
|
4862
4965
|
function buildConversationStats(manifest, snapshot) {
|
|
4863
4966
|
if (manifest.stats && manifest.status !== "active" && snapshot.messages.length === 0 && snapshot.runs.length === 0) {
|
|
4864
4967
|
return manifest.stats;
|
|
@@ -4973,12 +5076,21 @@ function readUsage(payload) {
|
|
|
4973
5076
|
return void 0;
|
|
4974
5077
|
}
|
|
4975
5078
|
const usage = toRecord4(payload.usage);
|
|
4976
|
-
const
|
|
4977
|
-
const
|
|
4978
|
-
const
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
5079
|
+
const response = toRecord4(payload.response);
|
|
5080
|
+
const responseUsage = toRecord4(response.usage);
|
|
5081
|
+
const context = firstRecord(
|
|
5082
|
+
payload.context,
|
|
5083
|
+
usage.context,
|
|
5084
|
+
response.context,
|
|
5085
|
+
responseUsage.context
|
|
5086
|
+
);
|
|
5087
|
+
const inputTokens = readInteger(usage, "input_tokens") ?? readInteger(usage, "prompt_tokens") ?? readInteger(responseUsage, "input_tokens") ?? readInteger(responseUsage, "prompt_tokens") ?? readInteger(payload, "input_tokens") ?? readInteger(payload, "prompt_tokens");
|
|
5088
|
+
const outputTokens = readInteger(usage, "output_tokens") ?? readInteger(usage, "completion_tokens") ?? readInteger(responseUsage, "output_tokens") ?? readInteger(responseUsage, "completion_tokens") ?? readInteger(payload, "output_tokens") ?? readInteger(payload, "completion_tokens");
|
|
5089
|
+
const totalTokens = readInteger(usage, "total_tokens") ?? readInteger(responseUsage, "total_tokens") ?? readInteger(payload, "total_tokens") ?? (inputTokens ?? 0) + (outputTokens ?? 0);
|
|
5090
|
+
const contextWindow = readInteger(context, "window_tokens") ?? readInteger(context, "windowTokens") ?? readInteger(context, "context_window") ?? readInteger(context, "context_max") ?? readInteger(context, "context_length") ?? readInteger(usage, "context_window") ?? readInteger(usage, "context_max") ?? readInteger(usage, "context_length") ?? readInteger(responseUsage, "context_window") ?? readInteger(responseUsage, "context_max") ?? readInteger(responseUsage, "context_length") ?? readInteger(response, "context_window") ?? readInteger(response, "context_max") ?? readInteger(response, "context_length") ?? readInteger(payload, "context_window") ?? readInteger(payload, "context_max") ?? readInteger(payload, "context_length");
|
|
5091
|
+
const explicitContextTokens = readInteger(context, "used_tokens") ?? readInteger(context, "usedTokens") ?? readInteger(context, "context_tokens") ?? readInteger(context, "context_used") ?? readInteger(context, "current_context_tokens") ?? readInteger(context, "last_prompt_tokens") ?? readInteger(usage, "context_tokens") ?? readInteger(usage, "context_used") ?? readInteger(usage, "current_context_tokens") ?? readInteger(usage, "last_prompt_tokens") ?? readInteger(responseUsage, "context_tokens") ?? readInteger(responseUsage, "context_used") ?? readInteger(responseUsage, "current_context_tokens") ?? readInteger(responseUsage, "last_prompt_tokens") ?? readInteger(response, "context_tokens") ?? readInteger(response, "context_used") ?? readInteger(response, "current_context_tokens") ?? readInteger(response, "last_prompt_tokens") ?? readInteger(payload, "context_tokens") ?? readInteger(payload, "context_used") ?? readInteger(payload, "current_context_tokens") ?? readInteger(payload, "last_prompt_tokens");
|
|
5092
|
+
const explicitUsagePercent = readInteger(context, "usage_percent") ?? readInteger(context, "context_percent") ?? readInteger(usage, "usage_percent") ?? readInteger(usage, "context_percent") ?? readInteger(responseUsage, "usage_percent") ?? readInteger(responseUsage, "context_percent") ?? readInteger(response, "usage_percent") ?? readInteger(response, "context_percent") ?? readInteger(payload, "usage_percent") ?? readInteger(payload, "context_percent");
|
|
5093
|
+
if (!inputTokens && !outputTokens && !totalTokens && explicitContextTokens === void 0) {
|
|
4982
5094
|
return void 0;
|
|
4983
5095
|
}
|
|
4984
5096
|
return {
|
|
@@ -4987,14 +5099,44 @@ function readUsage(payload) {
|
|
|
4987
5099
|
total_tokens: totalTokens,
|
|
4988
5100
|
...explicitContextTokens !== void 0 ? { context_tokens: explicitContextTokens } : {},
|
|
4989
5101
|
...contextWindow !== void 0 ? { context_window: contextWindow } : {},
|
|
5102
|
+
...explicitContextTokens !== void 0 ? { context_source: "explicit" } : {},
|
|
4990
5103
|
...explicitContextTokens !== void 0 && contextWindow ? {
|
|
4991
|
-
usage_percent: Math.min(
|
|
5104
|
+
usage_percent: explicitUsagePercent !== void 0 ? Math.min(100, explicitUsagePercent) : Math.min(
|
|
4992
5105
|
100,
|
|
4993
5106
|
Math.round(explicitContextTokens / contextWindow * 100)
|
|
4994
5107
|
)
|
|
4995
5108
|
} : {}
|
|
4996
5109
|
};
|
|
4997
5110
|
}
|
|
5111
|
+
function estimateContextUsage(input) {
|
|
5112
|
+
const currentInput = input.currentInput.trim();
|
|
5113
|
+
const instructions = input.instructions?.trim() ?? "";
|
|
5114
|
+
const messageCount = input.conversationHistory.length + (currentInput ? 1 : 0) + (instructions ? 1 : 0);
|
|
5115
|
+
if (messageCount === 0) {
|
|
5116
|
+
return void 0;
|
|
5117
|
+
}
|
|
5118
|
+
const serializedRequest = JSON.stringify({
|
|
5119
|
+
instructions: instructions || void 0,
|
|
5120
|
+
conversation_history: input.conversationHistory,
|
|
5121
|
+
input: currentInput || void 0
|
|
5122
|
+
});
|
|
5123
|
+
const estimatedTokens = Math.ceil(serializedRequest.length / ESTIMATED_CONTEXT_CHARS_PER_TOKEN) + ESTIMATED_CONTEXT_BASE_OVERHEAD_TOKENS + messageCount * ESTIMATED_CONTEXT_PER_MESSAGE_OVERHEAD_TOKENS;
|
|
5124
|
+
const contextTokens = input.contextWindow ? Math.min(input.contextWindow, estimatedTokens) : estimatedTokens;
|
|
5125
|
+
return {
|
|
5126
|
+
input_tokens: 0,
|
|
5127
|
+
output_tokens: 0,
|
|
5128
|
+
total_tokens: 0,
|
|
5129
|
+
context_tokens: contextTokens,
|
|
5130
|
+
...input.contextWindow !== void 0 ? { context_window: input.contextWindow } : {},
|
|
5131
|
+
...input.contextWindow ? {
|
|
5132
|
+
usage_percent: Math.min(
|
|
5133
|
+
100,
|
|
5134
|
+
Math.round(contextTokens / input.contextWindow * 100)
|
|
5135
|
+
)
|
|
5136
|
+
} : {},
|
|
5137
|
+
context_source: "estimated"
|
|
5138
|
+
};
|
|
5139
|
+
}
|
|
4998
5140
|
function isAgentRun(run) {
|
|
4999
5141
|
return run.kind !== "command";
|
|
5000
5142
|
}
|
|
@@ -5015,6 +5157,14 @@ function readInteger(payload, key) {
|
|
|
5015
5157
|
function toRecord4(value) {
|
|
5016
5158
|
return typeof value === "object" && value !== null ? value : {};
|
|
5017
5159
|
}
|
|
5160
|
+
function firstRecord(...values) {
|
|
5161
|
+
for (const value of values) {
|
|
5162
|
+
if (typeof value === "object" && value !== null) {
|
|
5163
|
+
return value;
|
|
5164
|
+
}
|
|
5165
|
+
}
|
|
5166
|
+
return {};
|
|
5167
|
+
}
|
|
5018
5168
|
|
|
5019
5169
|
// src/conversations/blob-store.ts
|
|
5020
5170
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
@@ -5730,7 +5880,7 @@ async function buildConversationRuntimeMetadata(paths, manifest, snapshot) {
|
|
|
5730
5880
|
};
|
|
5731
5881
|
const contextWindow = current.contextWindow ?? usage.context_window ?? usageRun?.context_window;
|
|
5732
5882
|
const contextTokens = usage.context_tokens;
|
|
5733
|
-
const contextSource = contextTokens === void 0 ? "unknown" : "explicit";
|
|
5883
|
+
const contextSource = contextTokens === void 0 ? "unknown" : usage.context_source ?? "explicit";
|
|
5734
5884
|
const provider = current.provider ?? usageRun?.provider;
|
|
5735
5885
|
const reasoningEffort = current.reasoningEffort;
|
|
5736
5886
|
return {
|
|
@@ -6324,12 +6474,12 @@ var ConversationCommandHandlers = class {
|
|
|
6324
6474
|
};
|
|
6325
6475
|
function formatContextUsageLines(runtime) {
|
|
6326
6476
|
const windowTokens = runtime.context.window_tokens ?? runtime.context.context_window;
|
|
6327
|
-
if (runtime.context.source === "explicit") {
|
|
6477
|
+
if (runtime.context.source === "explicit" || runtime.context.source === "estimated") {
|
|
6328
6478
|
const usedTokens = runtime.context.used_tokens ?? runtime.context.input_tokens;
|
|
6329
6479
|
const percent = runtime.context.usage_percent ?? (windowTokens && windowTokens > 0 ? Math.min(100, Math.round(usedTokens / windowTokens * 100)) : void 0);
|
|
6330
6480
|
return [
|
|
6331
6481
|
`\u4E0A\u4E0B\u6587\uFF1A${usedTokens}${windowTokens ? ` / ${windowTokens}` : ""}${percent === void 0 ? "" : `\uFF08${percent}%\uFF09`}`,
|
|
6332
|
-
"\u6765\u6E90\uFF1A\u6A21\u578B\u8FD4\u56DE"
|
|
6482
|
+
runtime.context.source === "estimated" ? "\u6765\u6E90\uFF1A\u672C\u5730\u4F30\u7B97" : "\u6765\u6E90\uFF1A\u6A21\u578B\u8FD4\u56DE"
|
|
6333
6483
|
];
|
|
6334
6484
|
}
|
|
6335
6485
|
return [
|
|
@@ -6703,9 +6853,7 @@ function isUsableLanIpv4(value) {
|
|
|
6703
6853
|
|
|
6704
6854
|
// src/hermes/session-title.ts
|
|
6705
6855
|
import { stat as stat6 } from "fs/promises";
|
|
6706
|
-
import { createRequire as createRequire2 } from "module";
|
|
6707
6856
|
import path11 from "path";
|
|
6708
|
-
var nodeRequire2 = createRequire2(import.meta.url);
|
|
6709
6857
|
async function readHermesSessionTitle(sessionId, paths, profileName) {
|
|
6710
6858
|
const trimmedSessionId = sessionId.trim();
|
|
6711
6859
|
if (!trimmedSessionId) {
|
|
@@ -6751,11 +6899,8 @@ async function readHermesCompressionTip(sessionId, paths, profileName) {
|
|
|
6751
6899
|
function readSessionTitleFromStateDb(dbPath, sessionId) {
|
|
6752
6900
|
let db = null;
|
|
6753
6901
|
try {
|
|
6754
|
-
|
|
6755
|
-
|
|
6756
|
-
);
|
|
6757
|
-
db = new DatabaseSync(dbPath, {
|
|
6758
|
-
readOnly: true,
|
|
6902
|
+
db = openSqliteDatabase(dbPath, {
|
|
6903
|
+
readonly: true,
|
|
6759
6904
|
timeout: 1e3
|
|
6760
6905
|
});
|
|
6761
6906
|
const row = db.prepare("SELECT title FROM sessions WHERE id = ? LIMIT 1").get(sessionId);
|
|
@@ -6770,11 +6915,8 @@ function readSessionTitleFromStateDb(dbPath, sessionId) {
|
|
|
6770
6915
|
function readCompressionTipFromStateDb(dbPath, sessionId) {
|
|
6771
6916
|
let db = null;
|
|
6772
6917
|
try {
|
|
6773
|
-
|
|
6774
|
-
|
|
6775
|
-
);
|
|
6776
|
-
db = new DatabaseSync(dbPath, {
|
|
6777
|
-
readOnly: true,
|
|
6918
|
+
db = openSqliteDatabase(dbPath, {
|
|
6919
|
+
readonly: true,
|
|
6778
6920
|
timeout: 1e3
|
|
6779
6921
|
});
|
|
6780
6922
|
let current = sessionId;
|
|
@@ -8234,12 +8376,63 @@ function toRecord6(value) {
|
|
|
8234
8376
|
}
|
|
8235
8377
|
|
|
8236
8378
|
// src/conversations/conversation-queries.ts
|
|
8379
|
+
var DEFAULT_CONVERSATION_LIST_PAGE_SIZE = 25;
|
|
8380
|
+
var MAX_CONVERSATION_LIST_PAGE_SIZE = 100;
|
|
8237
8381
|
var ConversationQueryCoordinator = class {
|
|
8238
8382
|
constructor(deps) {
|
|
8239
8383
|
this.deps = deps;
|
|
8240
8384
|
}
|
|
8241
8385
|
deps;
|
|
8242
8386
|
async listConversations() {
|
|
8387
|
+
return this.listConversationsFromStore();
|
|
8388
|
+
}
|
|
8389
|
+
async listConversationPage(options = {}) {
|
|
8390
|
+
const limit = normalizeConversationListPageLimit(options.limit);
|
|
8391
|
+
const cursor = decodeConversationListCursor(options.cursor);
|
|
8392
|
+
const indexedPage = await listConversationStatsPage(this.deps.paths, {
|
|
8393
|
+
limit,
|
|
8394
|
+
cursor
|
|
8395
|
+
});
|
|
8396
|
+
if (indexedPage.records.length === 0) {
|
|
8397
|
+
return this.listConversationPageFromStore({ limit, cursor });
|
|
8398
|
+
}
|
|
8399
|
+
const summaries = await this.summarizeIndexedConversations(
|
|
8400
|
+
indexedPage.records
|
|
8401
|
+
);
|
|
8402
|
+
return {
|
|
8403
|
+
conversations: summaries,
|
|
8404
|
+
page: {
|
|
8405
|
+
limit,
|
|
8406
|
+
has_more: indexedPage.hasMore,
|
|
8407
|
+
next_cursor: indexedPage.hasMore && indexedPage.records.length > 0 ? encodeConversationListCursor(indexedPage.records.at(-1)) : null
|
|
8408
|
+
}
|
|
8409
|
+
};
|
|
8410
|
+
}
|
|
8411
|
+
async searchConversationPage(options = {}) {
|
|
8412
|
+
const query = normalizeConversationSearchQuery(options.query);
|
|
8413
|
+
if (!query) {
|
|
8414
|
+
return this.listConversationPage(options);
|
|
8415
|
+
}
|
|
8416
|
+
const limit = normalizeConversationListPageLimit(options.limit);
|
|
8417
|
+
const cursor = decodeConversationListCursor(options.cursor);
|
|
8418
|
+
const indexedPage = await searchConversationStatsPage(this.deps.paths, {
|
|
8419
|
+
limit,
|
|
8420
|
+
cursor,
|
|
8421
|
+
query
|
|
8422
|
+
});
|
|
8423
|
+
const summaries = await this.summarizeIndexedConversations(
|
|
8424
|
+
indexedPage.records
|
|
8425
|
+
);
|
|
8426
|
+
return {
|
|
8427
|
+
conversations: summaries,
|
|
8428
|
+
page: {
|
|
8429
|
+
limit,
|
|
8430
|
+
has_more: indexedPage.hasMore,
|
|
8431
|
+
next_cursor: indexedPage.hasMore && indexedPage.records.length > 0 ? encodeConversationListCursor(indexedPage.records.at(-1)) : null
|
|
8432
|
+
}
|
|
8433
|
+
};
|
|
8434
|
+
}
|
|
8435
|
+
async listConversationsFromStore() {
|
|
8243
8436
|
const summaries = [];
|
|
8244
8437
|
for (const conversationId of await this.deps.store.listConversationIds()) {
|
|
8245
8438
|
const manifest = await this.deps.store.readManifest(conversationId).catch(
|
|
@@ -8255,9 +8448,41 @@ var ConversationQueryCoordinator = class {
|
|
|
8255
8448
|
summaries.push(await this.summarizeConversation(refreshed, snapshot));
|
|
8256
8449
|
}
|
|
8257
8450
|
return summaries.sort(
|
|
8258
|
-
(left, right) => Date.parse(right.updated_at) - Date.parse(left.updated_at)
|
|
8451
|
+
(left, right) => Date.parse(right.updated_at) - Date.parse(left.updated_at) || right.id.localeCompare(left.id)
|
|
8259
8452
|
);
|
|
8260
8453
|
}
|
|
8454
|
+
async listConversationPageFromStore(input) {
|
|
8455
|
+
const all = await this.listConversationsFromStore();
|
|
8456
|
+
const startIndex = input.cursor ? all.findIndex((summary) => isAfterConversationListCursor(summary, input.cursor)) : 0;
|
|
8457
|
+
const safeStartIndex = startIndex < 0 ? all.length : startIndex;
|
|
8458
|
+
const conversations = all.slice(safeStartIndex, safeStartIndex + input.limit);
|
|
8459
|
+
const hasMore = safeStartIndex + input.limit < all.length;
|
|
8460
|
+
return {
|
|
8461
|
+
conversations,
|
|
8462
|
+
page: {
|
|
8463
|
+
limit: input.limit,
|
|
8464
|
+
has_more: hasMore,
|
|
8465
|
+
next_cursor: hasMore && conversations.length > 0 ? encodeConversationListCursorFromSummary(conversations.at(-1)) : null
|
|
8466
|
+
}
|
|
8467
|
+
};
|
|
8468
|
+
}
|
|
8469
|
+
async summarizeIndexedConversations(records) {
|
|
8470
|
+
const summaries = [];
|
|
8471
|
+
for (const record of records) {
|
|
8472
|
+
const manifest = await this.deps.store.readManifest(record.conversationId).catch(
|
|
8473
|
+
() => null
|
|
8474
|
+
);
|
|
8475
|
+
if (!manifest || manifest.status !== "active") {
|
|
8476
|
+
continue;
|
|
8477
|
+
}
|
|
8478
|
+
const snapshot = await this.deps.store.readSnapshot(record.conversationId).catch(
|
|
8479
|
+
() => emptySnapshot2()
|
|
8480
|
+
);
|
|
8481
|
+
const refreshed = await this.deps.metadata.refreshTitleFromHermes(manifest, { snapshot }).catch(() => manifest);
|
|
8482
|
+
summaries.push(await this.summarizeConversation(refreshed, snapshot));
|
|
8483
|
+
}
|
|
8484
|
+
return summaries;
|
|
8485
|
+
}
|
|
8261
8486
|
async getMessages(conversationId, options = {}) {
|
|
8262
8487
|
const manifest = await this.deps.store.readActiveManifest(conversationId);
|
|
8263
8488
|
const snapshot = await this.deps.store.readSnapshot(conversationId);
|
|
@@ -8344,6 +8569,61 @@ var ConversationQueryCoordinator = class {
|
|
|
8344
8569
|
});
|
|
8345
8570
|
}
|
|
8346
8571
|
};
|
|
8572
|
+
function normalizeConversationListPageLimit(value) {
|
|
8573
|
+
if (value === void 0 || !Number.isFinite(value)) {
|
|
8574
|
+
return DEFAULT_CONVERSATION_LIST_PAGE_SIZE;
|
|
8575
|
+
}
|
|
8576
|
+
return Math.max(
|
|
8577
|
+
1,
|
|
8578
|
+
Math.min(MAX_CONVERSATION_LIST_PAGE_SIZE, Math.floor(value))
|
|
8579
|
+
);
|
|
8580
|
+
}
|
|
8581
|
+
function encodeConversationListCursor(record) {
|
|
8582
|
+
return Buffer.from(
|
|
8583
|
+
JSON.stringify({
|
|
8584
|
+
updated_at: record.updatedAt,
|
|
8585
|
+
conversation_id: record.conversationId
|
|
8586
|
+
}),
|
|
8587
|
+
"utf8"
|
|
8588
|
+
).toString("base64url");
|
|
8589
|
+
}
|
|
8590
|
+
function encodeConversationListCursorFromSummary(summary) {
|
|
8591
|
+
return encodeConversationListCursor({
|
|
8592
|
+
conversationId: summary.id,
|
|
8593
|
+
updatedAt: summary.updated_at
|
|
8594
|
+
});
|
|
8595
|
+
}
|
|
8596
|
+
function decodeConversationListCursor(value) {
|
|
8597
|
+
if (!value) {
|
|
8598
|
+
return null;
|
|
8599
|
+
}
|
|
8600
|
+
try {
|
|
8601
|
+
const decoded = JSON.parse(
|
|
8602
|
+
Buffer.from(value, "base64url").toString("utf8")
|
|
8603
|
+
);
|
|
8604
|
+
const updatedAt = readNonEmptyString(decoded.updated_at);
|
|
8605
|
+
const conversationId = readNonEmptyString(decoded.conversation_id);
|
|
8606
|
+
if (!updatedAt || !conversationId) {
|
|
8607
|
+
throw new Error("invalid cursor");
|
|
8608
|
+
}
|
|
8609
|
+
return { updatedAt, conversationId };
|
|
8610
|
+
} catch {
|
|
8611
|
+
throw new LinkHttpError(
|
|
8612
|
+
400,
|
|
8613
|
+
"conversation_cursor_invalid",
|
|
8614
|
+
"Conversation list cursor is invalid"
|
|
8615
|
+
);
|
|
8616
|
+
}
|
|
8617
|
+
}
|
|
8618
|
+
function readNonEmptyString(value) {
|
|
8619
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
8620
|
+
}
|
|
8621
|
+
function normalizeConversationSearchQuery(value) {
|
|
8622
|
+
return typeof value === "string" ? value.trim() : "";
|
|
8623
|
+
}
|
|
8624
|
+
function isAfterConversationListCursor(summary, cursor) {
|
|
8625
|
+
return summary.updated_at < cursor.updatedAt || summary.updated_at === cursor.updatedAt && summary.id < cursor.conversationId;
|
|
8626
|
+
}
|
|
8347
8627
|
function hydrateAgentEventBlocks(blocks, agentEvents) {
|
|
8348
8628
|
if (!blocks?.length || agentEvents.length === 0) {
|
|
8349
8629
|
return blocks;
|
|
@@ -8518,61 +8798,393 @@ function isNodeError8(error, code) {
|
|
|
8518
8798
|
|
|
8519
8799
|
// src/conversations/hermes-session-sync.ts
|
|
8520
8800
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
8521
|
-
import { readdir as
|
|
8522
|
-
import { createRequire as createRequire3 } from "module";
|
|
8801
|
+
import { readdir as readdir6, readFile as readFile9, stat as stat8 } from "fs/promises";
|
|
8523
8802
|
import os4 from "os";
|
|
8803
|
+
import path14 from "path";
|
|
8804
|
+
|
|
8805
|
+
// src/conversations/delivery-import.ts
|
|
8806
|
+
import { lstat, readFile as readFile8, readdir as readdir5, stat as stat7 } from "fs/promises";
|
|
8524
8807
|
import path13 from "path";
|
|
8525
|
-
var
|
|
8526
|
-
var
|
|
8527
|
-
var
|
|
8528
|
-
var
|
|
8529
|
-
var
|
|
8530
|
-
|
|
8531
|
-
|
|
8532
|
-
"
|
|
8533
|
-
"
|
|
8534
|
-
"
|
|
8535
|
-
"
|
|
8536
|
-
"
|
|
8537
|
-
"
|
|
8538
|
-
"
|
|
8539
|
-
"
|
|
8540
|
-
"
|
|
8541
|
-
"
|
|
8542
|
-
"
|
|
8543
|
-
"
|
|
8544
|
-
"
|
|
8545
|
-
"
|
|
8546
|
-
|
|
8547
|
-
|
|
8548
|
-
|
|
8549
|
-
|
|
8550
|
-
|
|
8551
|
-
|
|
8552
|
-
|
|
8553
|
-
|
|
8554
|
-
|
|
8555
|
-
|
|
8556
|
-
|
|
8557
|
-
|
|
8558
|
-
|
|
8559
|
-
|
|
8560
|
-
|
|
8561
|
-
|
|
8562
|
-
|
|
8808
|
+
var MAX_IMPORTED_BLOB_BYTES = 100 * 1024 * 1024;
|
|
8809
|
+
var MAX_MEDIA_IMPORT_FAILURES = 20;
|
|
8810
|
+
var MAX_DELIVERY_FILES = 50;
|
|
8811
|
+
var DELIVERY_STAGING_SEGMENT = "delivery-staging";
|
|
8812
|
+
var SUPPORTED_DELIVERY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
8813
|
+
".png",
|
|
8814
|
+
".jpg",
|
|
8815
|
+
".jpeg",
|
|
8816
|
+
".gif",
|
|
8817
|
+
".webp",
|
|
8818
|
+
".heic",
|
|
8819
|
+
".pdf",
|
|
8820
|
+
".txt",
|
|
8821
|
+
".log",
|
|
8822
|
+
".md",
|
|
8823
|
+
".markdown",
|
|
8824
|
+
".json",
|
|
8825
|
+
".jsonl",
|
|
8826
|
+
".yaml",
|
|
8827
|
+
".yml",
|
|
8828
|
+
".toml",
|
|
8829
|
+
".ini",
|
|
8830
|
+
".xml",
|
|
8831
|
+
".html",
|
|
8832
|
+
".css",
|
|
8833
|
+
".js",
|
|
8834
|
+
".ts",
|
|
8835
|
+
".jsx",
|
|
8836
|
+
".tsx",
|
|
8837
|
+
".dart",
|
|
8838
|
+
".py",
|
|
8839
|
+
".java",
|
|
8840
|
+
".kt",
|
|
8841
|
+
".swift",
|
|
8842
|
+
".go",
|
|
8843
|
+
".rs",
|
|
8844
|
+
".rb",
|
|
8845
|
+
".php",
|
|
8846
|
+
".c",
|
|
8847
|
+
".cc",
|
|
8848
|
+
".cpp",
|
|
8849
|
+
".h",
|
|
8850
|
+
".hpp",
|
|
8851
|
+
".cs",
|
|
8852
|
+
".sql",
|
|
8853
|
+
".csv",
|
|
8854
|
+
".tsv",
|
|
8855
|
+
".doc",
|
|
8856
|
+
".docx",
|
|
8857
|
+
".xls",
|
|
8858
|
+
".xlsx",
|
|
8859
|
+
".ppt",
|
|
8860
|
+
".pptx",
|
|
8861
|
+
".zip",
|
|
8862
|
+
".rar",
|
|
8863
|
+
".7z",
|
|
8864
|
+
".tar",
|
|
8865
|
+
".gz",
|
|
8866
|
+
".mp4",
|
|
8867
|
+
".mov",
|
|
8868
|
+
".avi",
|
|
8869
|
+
".mkv",
|
|
8870
|
+
".webm",
|
|
8871
|
+
".ogg",
|
|
8872
|
+
".opus",
|
|
8873
|
+
".mp3",
|
|
8874
|
+
".wav",
|
|
8875
|
+
".m4a"
|
|
8876
|
+
]);
|
|
8877
|
+
function resolveDeliveryStagingTarget(paths, stagingDir) {
|
|
8878
|
+
const resolvedDir = path13.resolve(stagingDir);
|
|
8879
|
+
const relative = path13.relative(path13.resolve(paths.conversationsDir), resolvedDir);
|
|
8880
|
+
if (!relative || relative.startsWith("..") || path13.isAbsolute(relative)) {
|
|
8881
|
+
throw new LinkHttpError(
|
|
8882
|
+
400,
|
|
8883
|
+
"delivery_staging_invalid",
|
|
8884
|
+
"delivery staging directory must be inside Hermes Link conversations"
|
|
8885
|
+
);
|
|
8886
|
+
}
|
|
8887
|
+
const segments = relative.split(path13.sep);
|
|
8888
|
+
if (segments.length !== 3 || segments[1] !== DELIVERY_STAGING_SEGMENT || !segments[0] || !segments[2]) {
|
|
8889
|
+
throw new LinkHttpError(
|
|
8890
|
+
400,
|
|
8891
|
+
"delivery_staging_invalid",
|
|
8892
|
+
"delivery staging directory is invalid"
|
|
8893
|
+
);
|
|
8894
|
+
}
|
|
8895
|
+
return {
|
|
8896
|
+
conversationId: segments[0],
|
|
8897
|
+
runId: segments[2],
|
|
8898
|
+
stagingDir: resolvedDir
|
|
8563
8899
|
};
|
|
8564
|
-
|
|
8565
|
-
|
|
8566
|
-
|
|
8567
|
-
|
|
8568
|
-
|
|
8569
|
-
|
|
8570
|
-
|
|
8571
|
-
|
|
8572
|
-
|
|
8573
|
-
|
|
8574
|
-
|
|
8575
|
-
|
|
8900
|
+
}
|
|
8901
|
+
async function collectStagedDeliveryReferences(stagingDir) {
|
|
8902
|
+
const directoryStat = await lstat(stagingDir).catch((error) => {
|
|
8903
|
+
if (isNodeError9(error, "ENOENT")) {
|
|
8904
|
+
throw new LinkHttpError(
|
|
8905
|
+
404,
|
|
8906
|
+
"delivery_staging_not_found",
|
|
8907
|
+
"delivery staging directory was not found"
|
|
8908
|
+
);
|
|
8909
|
+
}
|
|
8910
|
+
throw error;
|
|
8911
|
+
});
|
|
8912
|
+
if (!directoryStat.isDirectory()) {
|
|
8913
|
+
throw new LinkHttpError(
|
|
8914
|
+
400,
|
|
8915
|
+
"delivery_staging_not_directory",
|
|
8916
|
+
"delivery staging path is not a directory"
|
|
8917
|
+
);
|
|
8918
|
+
}
|
|
8919
|
+
const entries = await readdir5(stagingDir, { withFileTypes: true });
|
|
8920
|
+
return entries.filter((entry) => entry.isFile() && !entry.name.startsWith(".")).filter((entry) => isSupportedDeliveryFilename(entry.name)).sort(
|
|
8921
|
+
(left, right) => left.name.localeCompare(right.name, "en", { numeric: true })
|
|
8922
|
+
).slice(0, MAX_DELIVERY_FILES).map((entry) => {
|
|
8923
|
+
const sourcePath = path13.join(stagingDir, entry.name);
|
|
8924
|
+
const mime = inferMimeType(sourcePath);
|
|
8925
|
+
return {
|
|
8926
|
+
path: sourcePath,
|
|
8927
|
+
kind: mediaKindForMime(mime),
|
|
8928
|
+
mime
|
|
8929
|
+
};
|
|
8930
|
+
});
|
|
8931
|
+
}
|
|
8932
|
+
async function importMediaReferencesForMessage(deps, input) {
|
|
8933
|
+
const references = input.references.slice(0, input.maxReferences ?? MAX_DELIVERY_FILES);
|
|
8934
|
+
if (references.length === 0) {
|
|
8935
|
+
return emptyImportResult(input);
|
|
8936
|
+
}
|
|
8937
|
+
const snapshot = await deps.readSnapshot(input.conversationId);
|
|
8938
|
+
const assistant = snapshot.messages.find(
|
|
8939
|
+
(message) => message.id === input.messageId
|
|
8940
|
+
);
|
|
8941
|
+
if (!assistant) {
|
|
8942
|
+
return emptyImportResult(input);
|
|
8943
|
+
}
|
|
8944
|
+
const importedSourceKeys = readImportedMediaSourceKeys(assistant);
|
|
8945
|
+
const failedSourceKeys = readFailedMediaSourceKeys(assistant);
|
|
8946
|
+
const failureRecordsByKey = new Map(
|
|
8947
|
+
readMediaImportFailures(assistant).map((failure) => [failure.key, failure])
|
|
8948
|
+
);
|
|
8949
|
+
const importedParts = [];
|
|
8950
|
+
const newFailures = [];
|
|
8951
|
+
let skippedCount = 0;
|
|
8952
|
+
for (const reference of references) {
|
|
8953
|
+
let sourceKey;
|
|
8954
|
+
try {
|
|
8955
|
+
sourceKey = mediaSourceKey(reference.path);
|
|
8956
|
+
if (importedSourceKeys.has(sourceKey) || failedSourceKeys.has(sourceKey)) {
|
|
8957
|
+
skippedCount += 1;
|
|
8958
|
+
continue;
|
|
8959
|
+
}
|
|
8960
|
+
const blob = await writeBlobFromFile(deps, input.conversationId, reference);
|
|
8961
|
+
const part = {
|
|
8962
|
+
type: reference.kind ?? mediaKindForMime(blob.mime),
|
|
8963
|
+
blob: blob.id,
|
|
8964
|
+
mime: blob.mime,
|
|
8965
|
+
size: blob.size,
|
|
8966
|
+
filename: blob.filename,
|
|
8967
|
+
url: `/api/v1/conversations/${encodeURIComponent(input.conversationId)}/blobs/${encodeURIComponent(blob.id)}`
|
|
8968
|
+
};
|
|
8969
|
+
assistant.parts.push(part);
|
|
8970
|
+
assistant.attachments.push({
|
|
8971
|
+
blob_id: blob.id,
|
|
8972
|
+
mime: blob.mime,
|
|
8973
|
+
size: blob.size,
|
|
8974
|
+
filename: blob.filename,
|
|
8975
|
+
source: "hermes_output"
|
|
8976
|
+
});
|
|
8977
|
+
importedSourceKeys.add(sourceKey);
|
|
8978
|
+
importedParts.push(part);
|
|
8979
|
+
} catch (error) {
|
|
8980
|
+
if (sourceKey && !failedSourceKeys.has(sourceKey)) {
|
|
8981
|
+
const failure = describeMediaImportFailure(reference, sourceKey, error);
|
|
8982
|
+
failedSourceKeys.add(sourceKey);
|
|
8983
|
+
failureRecordsByKey.set(sourceKey, failure);
|
|
8984
|
+
newFailures.push(failure);
|
|
8985
|
+
}
|
|
8986
|
+
void deps.logger.warn("conversation_media_import_failed", {
|
|
8987
|
+
conversation_id: input.conversationId,
|
|
8988
|
+
run_id: input.runId,
|
|
8989
|
+
message_id: input.messageId,
|
|
8990
|
+
error: error instanceof Error ? error.message : String(error)
|
|
8991
|
+
});
|
|
8992
|
+
}
|
|
8993
|
+
}
|
|
8994
|
+
if (importedParts.length === 0 && newFailures.length === 0) {
|
|
8995
|
+
return {
|
|
8996
|
+
...emptyImportResult(input),
|
|
8997
|
+
discovered_count: references.length,
|
|
8998
|
+
skipped_count: skippedCount
|
|
8999
|
+
};
|
|
9000
|
+
}
|
|
9001
|
+
assistant.hermes = {
|
|
9002
|
+
...toRecord7(assistant.hermes),
|
|
9003
|
+
imported_media_source_keys: [...importedSourceKeys],
|
|
9004
|
+
media_import_failed_source_keys: [...failedSourceKeys],
|
|
9005
|
+
media_import_failures: [...failureRecordsByKey.values()].slice(
|
|
9006
|
+
-MAX_MEDIA_IMPORT_FAILURES
|
|
9007
|
+
)
|
|
9008
|
+
};
|
|
9009
|
+
assistant.updated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
9010
|
+
await deps.writeSnapshot(input.conversationId, snapshot);
|
|
9011
|
+
let lastEventSeq;
|
|
9012
|
+
if (importedParts.length > 0) {
|
|
9013
|
+
const event = await deps.appendEvent(input.conversationId, {
|
|
9014
|
+
type: "message.parts.created",
|
|
9015
|
+
message_id: input.messageId,
|
|
9016
|
+
run_id: input.runId,
|
|
9017
|
+
payload: { parts: importedParts }
|
|
9018
|
+
});
|
|
9019
|
+
lastEventSeq = event.seq;
|
|
9020
|
+
}
|
|
9021
|
+
return {
|
|
9022
|
+
conversation_id: input.conversationId,
|
|
9023
|
+
run_id: input.runId,
|
|
9024
|
+
message_id: input.messageId,
|
|
9025
|
+
discovered_count: references.length,
|
|
9026
|
+
imported_count: importedParts.length,
|
|
9027
|
+
skipped_count: skippedCount,
|
|
9028
|
+
failed_count: newFailures.length,
|
|
9029
|
+
parts: importedParts,
|
|
9030
|
+
...lastEventSeq ? { last_event_seq: lastEventSeq } : {}
|
|
9031
|
+
};
|
|
9032
|
+
}
|
|
9033
|
+
function readMediaImportFailures(message) {
|
|
9034
|
+
const hermes = toRecord7(message.hermes);
|
|
9035
|
+
const failures = hermes.media_import_failures;
|
|
9036
|
+
if (!Array.isArray(failures)) {
|
|
9037
|
+
return [];
|
|
9038
|
+
}
|
|
9039
|
+
return failures.flatMap((item) => {
|
|
9040
|
+
const record = toRecord7(item);
|
|
9041
|
+
const key = readString8(record, "key");
|
|
9042
|
+
const filename = readString8(record, "filename");
|
|
9043
|
+
const reason = readString8(record, "reason");
|
|
9044
|
+
if (!key || !filename || !reason) {
|
|
9045
|
+
return [];
|
|
9046
|
+
}
|
|
9047
|
+
return [
|
|
9048
|
+
{
|
|
9049
|
+
key,
|
|
9050
|
+
filename,
|
|
9051
|
+
reason,
|
|
9052
|
+
...readString8(record, "code") ? { code: readString8(record, "code") } : {}
|
|
9053
|
+
}
|
|
9054
|
+
];
|
|
9055
|
+
});
|
|
9056
|
+
}
|
|
9057
|
+
function readFailedMediaSourceKeys(message) {
|
|
9058
|
+
const hermes = toRecord7(message.hermes);
|
|
9059
|
+
const keys = hermes.media_import_failed_source_keys;
|
|
9060
|
+
if (!Array.isArray(keys)) {
|
|
9061
|
+
return /* @__PURE__ */ new Set();
|
|
9062
|
+
}
|
|
9063
|
+
return new Set(
|
|
9064
|
+
keys.filter(
|
|
9065
|
+
(key) => typeof key === "string" && key.length > 0
|
|
9066
|
+
)
|
|
9067
|
+
);
|
|
9068
|
+
}
|
|
9069
|
+
function emptyImportResult(input) {
|
|
9070
|
+
return {
|
|
9071
|
+
conversation_id: input.conversationId,
|
|
9072
|
+
run_id: input.runId,
|
|
9073
|
+
message_id: input.messageId,
|
|
9074
|
+
discovered_count: 0,
|
|
9075
|
+
imported_count: 0,
|
|
9076
|
+
skipped_count: 0,
|
|
9077
|
+
failed_count: 0,
|
|
9078
|
+
parts: []
|
|
9079
|
+
};
|
|
9080
|
+
}
|
|
9081
|
+
async function writeBlobFromFile(deps, conversationId, source) {
|
|
9082
|
+
const sourcePath = resolveMediaSourcePath(source.path);
|
|
9083
|
+
const fileStat = await stat7(sourcePath).catch((error) => {
|
|
9084
|
+
if (isNodeError9(error, "ENOENT")) {
|
|
9085
|
+
throw new LinkHttpError(
|
|
9086
|
+
404,
|
|
9087
|
+
"media_source_not_found",
|
|
9088
|
+
"Hermes output file was not found"
|
|
9089
|
+
);
|
|
9090
|
+
}
|
|
9091
|
+
throw error;
|
|
9092
|
+
});
|
|
9093
|
+
if (!fileStat.isFile()) {
|
|
9094
|
+
throw new LinkHttpError(
|
|
9095
|
+
400,
|
|
9096
|
+
"media_source_not_file",
|
|
9097
|
+
"Hermes output media source is not a file"
|
|
9098
|
+
);
|
|
9099
|
+
}
|
|
9100
|
+
if (fileStat.size > MAX_IMPORTED_BLOB_BYTES) {
|
|
9101
|
+
throw new LinkHttpError(
|
|
9102
|
+
413,
|
|
9103
|
+
"media_source_too_large",
|
|
9104
|
+
"Hermes output media source is too large"
|
|
9105
|
+
);
|
|
9106
|
+
}
|
|
9107
|
+
return deps.writeBlob(conversationId, {
|
|
9108
|
+
bytes: await readFile8(sourcePath),
|
|
9109
|
+
filename: path13.basename(sourcePath),
|
|
9110
|
+
mime: source.mime ?? inferMimeType(sourcePath)
|
|
9111
|
+
});
|
|
9112
|
+
}
|
|
9113
|
+
function describeMediaImportFailure(reference, sourceKey, error) {
|
|
9114
|
+
return {
|
|
9115
|
+
key: sourceKey,
|
|
9116
|
+
filename: sanitizeFilename(reference.path, "attachment"),
|
|
9117
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
9118
|
+
...isNodeError9(error) && error.code ? { code: error.code } : {}
|
|
9119
|
+
};
|
|
9120
|
+
}
|
|
9121
|
+
function isSupportedDeliveryFilename(filename) {
|
|
9122
|
+
return SUPPORTED_DELIVERY_EXTENSIONS.has(path13.extname(filename).toLowerCase());
|
|
9123
|
+
}
|
|
9124
|
+
function readString8(payload, key) {
|
|
9125
|
+
const value = payload[key];
|
|
9126
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
9127
|
+
}
|
|
9128
|
+
function toRecord7(value) {
|
|
9129
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
9130
|
+
}
|
|
9131
|
+
function isNodeError9(error, code) {
|
|
9132
|
+
return typeof error === "object" && error !== null && "code" in error && (code === void 0 || error.code === code);
|
|
9133
|
+
}
|
|
9134
|
+
|
|
9135
|
+
// src/conversations/hermes-session-sync.ts
|
|
9136
|
+
var PROFILE_NAME_PATTERN3 = /^[a-zA-Z0-9._-]{1,64}$/u;
|
|
9137
|
+
var DEFAULT_PROFILE_NAME = "default";
|
|
9138
|
+
var MAX_IMPORTABLE_SESSIONS = 100;
|
|
9139
|
+
var HIDDEN_SESSION_SOURCES = /* @__PURE__ */ new Set(["tool"]);
|
|
9140
|
+
var HERMES_IMPORT_PROJECTION_VERSION = "turn_blocks_v3";
|
|
9141
|
+
var IMPORTED_MEDIA_PLACEHOLDER_RUN_ID = "imported_from_hermes";
|
|
9142
|
+
var MAX_IMPORTED_HERMES_MEDIA_BYTES = 100 * 1024 * 1024;
|
|
9143
|
+
var MESSAGE_COLUMNS = [
|
|
9144
|
+
"id",
|
|
9145
|
+
"session_id",
|
|
9146
|
+
"role",
|
|
9147
|
+
"content",
|
|
9148
|
+
"tool_call_id",
|
|
9149
|
+
"tool_calls",
|
|
9150
|
+
"tool_name",
|
|
9151
|
+
"timestamp",
|
|
9152
|
+
"token_count",
|
|
9153
|
+
"finish_reason",
|
|
9154
|
+
"reasoning",
|
|
9155
|
+
"reasoning_content",
|
|
9156
|
+
"reasoning_details",
|
|
9157
|
+
"codex_reasoning_items"
|
|
9158
|
+
];
|
|
9159
|
+
async function syncHermesSessionsIntoConversations(paths, logger, options = {}) {
|
|
9160
|
+
const maxImports = options.maxImports ?? MAX_IMPORTABLE_SESSIONS;
|
|
9161
|
+
const store = new ConversationStore(paths);
|
|
9162
|
+
const knownHermesSessions = await readKnownHermesSessions(store);
|
|
9163
|
+
const profileNames = await discoverHermesProfileNames();
|
|
9164
|
+
const result = {
|
|
9165
|
+
scanned_profiles: profileNames.length,
|
|
9166
|
+
scanned_sessions: 0,
|
|
9167
|
+
eligible_sessions: 0,
|
|
9168
|
+
imported_count: 0,
|
|
9169
|
+
reprojected_count: 0,
|
|
9170
|
+
skipped_existing: 0,
|
|
9171
|
+
skipped_hidden: 0,
|
|
9172
|
+
skipped_deleted: 0,
|
|
9173
|
+
skipped_over_limit: 0,
|
|
9174
|
+
errors: []
|
|
9175
|
+
};
|
|
9176
|
+
const candidates = [];
|
|
9177
|
+
for (const profileName of profileNames) {
|
|
9178
|
+
const profileDir = resolveHermesProfileDir(profileName);
|
|
9179
|
+
const dbPath = path14.join(profileDir, "state.db");
|
|
9180
|
+
const sessions = await listProfileSessions(dbPath).catch((error) => {
|
|
9181
|
+
result.errors.push({
|
|
9182
|
+
profile: profileName,
|
|
9183
|
+
message: error instanceof Error ? error.message : String(error)
|
|
9184
|
+
});
|
|
9185
|
+
return [];
|
|
9186
|
+
});
|
|
9187
|
+
result.scanned_sessions += sessions.length;
|
|
8576
9188
|
for (const session of sessions) {
|
|
8577
9189
|
if (isDeletedSession(session)) {
|
|
8578
9190
|
result.skipped_deleted += 1;
|
|
@@ -8599,6 +9211,7 @@ async function syncHermesSessionsIntoConversations(paths, logger, options = {})
|
|
|
8599
9211
|
const reprojected = await reprojectExistingHermesConversation({
|
|
8600
9212
|
paths,
|
|
8601
9213
|
store,
|
|
9214
|
+
logger,
|
|
8602
9215
|
candidate,
|
|
8603
9216
|
conversationIds: knownHermesSessions.conversationIdsBySessionId.get(
|
|
8604
9217
|
candidate.session.id
|
|
@@ -8618,6 +9231,7 @@ async function syncHermesSessionsIntoConversations(paths, logger, options = {})
|
|
|
8618
9231
|
const imported = await importHermesSession({
|
|
8619
9232
|
paths,
|
|
8620
9233
|
store,
|
|
9234
|
+
logger,
|
|
8621
9235
|
candidate,
|
|
8622
9236
|
existingHermesSessionIds: knownHermesSessions.ids
|
|
8623
9237
|
}).catch((error) => {
|
|
@@ -8639,7 +9253,7 @@ async function syncHermesSessionsIntoConversations(paths, logger, options = {})
|
|
|
8639
9253
|
return result;
|
|
8640
9254
|
}
|
|
8641
9255
|
async function importHermesSession(input) {
|
|
8642
|
-
const { paths, store, candidate, existingHermesSessionIds } = input;
|
|
9256
|
+
const { paths, store, logger, candidate, existingHermesSessionIds } = input;
|
|
8643
9257
|
const profile = await resolveConversationProfileTarget(
|
|
8644
9258
|
paths,
|
|
8645
9259
|
candidate.profileName
|
|
@@ -8662,7 +9276,7 @@ async function importHermesSession(input) {
|
|
|
8662
9276
|
}),
|
|
8663
9277
|
runs: []
|
|
8664
9278
|
};
|
|
8665
|
-
const title =
|
|
9279
|
+
const title = readString9(candidate.session, "title") ?? firstUserText(snapshot);
|
|
8666
9280
|
const manifest = {
|
|
8667
9281
|
id: conversationId,
|
|
8668
9282
|
schema_version: 1,
|
|
@@ -8680,6 +9294,13 @@ async function importHermesSession(input) {
|
|
|
8680
9294
|
last_event_seq: 0
|
|
8681
9295
|
};
|
|
8682
9296
|
await store.createConversation(manifest, snapshot);
|
|
9297
|
+
await hydrateImportedConversationMedia({
|
|
9298
|
+
paths,
|
|
9299
|
+
store,
|
|
9300
|
+
logger,
|
|
9301
|
+
conversationId
|
|
9302
|
+
});
|
|
9303
|
+
const hydratedSnapshot = await store.readSnapshot(conversationId);
|
|
8683
9304
|
await store.appendEvent(conversationId, {
|
|
8684
9305
|
type: "conversation.created",
|
|
8685
9306
|
payload: {
|
|
@@ -8693,7 +9314,7 @@ async function importHermesSession(input) {
|
|
|
8693
9314
|
}
|
|
8694
9315
|
}
|
|
8695
9316
|
});
|
|
8696
|
-
for (const message of
|
|
9317
|
+
for (const message of hydratedSnapshot.messages) {
|
|
8697
9318
|
await store.appendEvent(conversationId, {
|
|
8698
9319
|
type: "message.created",
|
|
8699
9320
|
message_id: message.id,
|
|
@@ -8703,7 +9324,7 @@ async function importHermesSession(input) {
|
|
|
8703
9324
|
}
|
|
8704
9325
|
const stats = buildConversationStats(
|
|
8705
9326
|
await store.readManifest(conversationId),
|
|
8706
|
-
|
|
9327
|
+
hydratedSnapshot
|
|
8707
9328
|
);
|
|
8708
9329
|
await store.writeManifest({
|
|
8709
9330
|
...await store.readManifest(conversationId),
|
|
@@ -8749,8 +9370,15 @@ async function reprojectExistingHermesConversation(input) {
|
|
|
8749
9370
|
...snapshot.messages.slice(prefix.endIndex)
|
|
8750
9371
|
]
|
|
8751
9372
|
};
|
|
8752
|
-
const stats = buildConversationStats(manifest, nextSnapshot);
|
|
8753
9373
|
await input.store.writeSnapshot(conversationId, nextSnapshot);
|
|
9374
|
+
await hydrateImportedConversationMedia({
|
|
9375
|
+
paths: input.paths,
|
|
9376
|
+
store: input.store,
|
|
9377
|
+
logger: input.logger,
|
|
9378
|
+
conversationId
|
|
9379
|
+
});
|
|
9380
|
+
const hydratedSnapshot = await input.store.readSnapshot(conversationId);
|
|
9381
|
+
const stats = buildConversationStats(manifest, hydratedSnapshot);
|
|
8754
9382
|
await input.store.writeManifest({ ...manifest, stats });
|
|
8755
9383
|
await upsertConversationStats(
|
|
8756
9384
|
input.paths,
|
|
@@ -8765,6 +9393,7 @@ function collectImportedHermesPrefix(snapshot) {
|
|
|
8765
9393
|
const seen = /* @__PURE__ */ new Set();
|
|
8766
9394
|
let needsProjectionVersion = false;
|
|
8767
9395
|
let hasToolMetadata = false;
|
|
9396
|
+
let hasMediaDeliveryMarkup = false;
|
|
8768
9397
|
let endIndex = 0;
|
|
8769
9398
|
for (; endIndex < snapshot.messages.length; endIndex += 1) {
|
|
8770
9399
|
const message = snapshot.messages[endIndex];
|
|
@@ -8782,6 +9411,9 @@ function collectImportedHermesPrefix(snapshot) {
|
|
|
8782
9411
|
if (hasHermesToolMetadata(row)) {
|
|
8783
9412
|
hasToolMetadata = true;
|
|
8784
9413
|
}
|
|
9414
|
+
if (collectMediaTags(normalizeContent(row.content)).length > 0) {
|
|
9415
|
+
hasMediaDeliveryMarkup = true;
|
|
9416
|
+
}
|
|
8785
9417
|
}
|
|
8786
9418
|
for (const event of message.agent_events ?? []) {
|
|
8787
9419
|
for (const row of readHermesRowsFromAgentEvent(event)) {
|
|
@@ -8789,6 +9421,9 @@ function collectImportedHermesPrefix(snapshot) {
|
|
|
8789
9421
|
if (hasHermesToolMetadata(row)) {
|
|
8790
9422
|
hasToolMetadata = true;
|
|
8791
9423
|
}
|
|
9424
|
+
if (collectMediaTags(normalizeContent(row.content)).length > 0) {
|
|
9425
|
+
hasMediaDeliveryMarkup = true;
|
|
9426
|
+
}
|
|
8792
9427
|
}
|
|
8793
9428
|
}
|
|
8794
9429
|
}
|
|
@@ -8798,7 +9433,7 @@ function collectImportedHermesPrefix(snapshot) {
|
|
|
8798
9433
|
return {
|
|
8799
9434
|
endIndex,
|
|
8800
9435
|
messages: rows,
|
|
8801
|
-
needsUpgrade: needsProjectionVersion && hasToolMetadata
|
|
9436
|
+
needsUpgrade: needsProjectionVersion && (hasToolMetadata || hasMediaDeliveryMarkup)
|
|
8802
9437
|
};
|
|
8803
9438
|
}
|
|
8804
9439
|
function isHermesImportedMessage(message) {
|
|
@@ -8808,13 +9443,13 @@ function readHermesRowsFromAgentEvent(event) {
|
|
|
8808
9443
|
if (event.raw?.format !== "hermes-message" && event.raw?.format !== "hermes-message-group") {
|
|
8809
9444
|
return [];
|
|
8810
9445
|
}
|
|
8811
|
-
const payload =
|
|
8812
|
-
const message =
|
|
8813
|
-
if (normalizeMessageRole(
|
|
9446
|
+
const payload = toRecord8(event.raw.payload);
|
|
9447
|
+
const message = toRecord8(payload.message);
|
|
9448
|
+
if (normalizeMessageRole(readString9(message, "role") ?? void 0) === "tool") {
|
|
8814
9449
|
return [message];
|
|
8815
9450
|
}
|
|
8816
9451
|
return readHermesRawMessageRows(event.raw).filter(
|
|
8817
|
-
(row) => Boolean(
|
|
9452
|
+
(row) => Boolean(readString9(row, "role"))
|
|
8818
9453
|
);
|
|
8819
9454
|
}
|
|
8820
9455
|
function appendHermesRowOnce(rows, seen, row) {
|
|
@@ -8832,7 +9467,7 @@ function hermesRowKey(row, fallbackIndex) {
|
|
|
8832
9467
|
return `fallback:${fallbackIndex}:${row.role ?? ""}:${row.timestamp ?? ""}:${normalizeContent(row.content)}`;
|
|
8833
9468
|
}
|
|
8834
9469
|
function hasHermesToolMetadata(row) {
|
|
8835
|
-
return normalizeMessageRole(row.role) === "tool" || readHermesToolCalls(row).length > 0 || Boolean(
|
|
9470
|
+
return normalizeMessageRole(row.role) === "tool" || readHermesToolCalls(row).length > 0 || Boolean(readString9(row, "tool_call_id")) || Boolean(readString9(row, "tool_name"));
|
|
8836
9471
|
}
|
|
8837
9472
|
function toLinkMessages(input) {
|
|
8838
9473
|
const linkMessages = [];
|
|
@@ -8950,8 +9585,8 @@ function toLinkMessages(input) {
|
|
|
8950
9585
|
return linkMessages;
|
|
8951
9586
|
}
|
|
8952
9587
|
function consumePendingToolCall(input) {
|
|
8953
|
-
const toolCallId =
|
|
8954
|
-
const toolName =
|
|
9588
|
+
const toolCallId = readString9(input.toolMessage, "tool_call_id");
|
|
9589
|
+
const toolName = readString9(input.toolMessage, "tool_name");
|
|
8955
9590
|
let pending = toolCallId ? input.toolCallsById.get(toolCallId) : void 0;
|
|
8956
9591
|
if (!pending && toolName) {
|
|
8957
9592
|
pending = input.pendingToolCalls.find(
|
|
@@ -9001,13 +9636,13 @@ function readHermesToolCalls(message) {
|
|
|
9001
9636
|
);
|
|
9002
9637
|
}
|
|
9003
9638
|
function normalizeHermesToolCall(value) {
|
|
9004
|
-
const record =
|
|
9639
|
+
const record = toRecord8(value);
|
|
9005
9640
|
if (Object.keys(record).length === 0) {
|
|
9006
9641
|
return null;
|
|
9007
9642
|
}
|
|
9008
|
-
const fn =
|
|
9009
|
-
const id =
|
|
9010
|
-
const name =
|
|
9643
|
+
const fn = toRecord8(record.function);
|
|
9644
|
+
const id = readString9(record, "id") ?? readString9(record, "call_id") ?? readString9(record, "tool_call_id") ?? readString9(fn, "id") ?? void 0;
|
|
9645
|
+
const name = readString9(fn, "name") ?? readString9(record, "name") ?? readString9(record, "tool_name") ?? readString9(record, "tool") ?? "tool";
|
|
9011
9646
|
const rawArguments = fn.arguments ?? record.arguments ?? record.args ?? record.input;
|
|
9012
9647
|
return {
|
|
9013
9648
|
...id ? { id } : {},
|
|
@@ -9046,8 +9681,8 @@ function projectHermesToolCompletedEvent(input) {
|
|
|
9046
9681
|
const createdAt = isoFromHermesTime(input.sourceMessage.timestamp) ?? new Date(Date.now() + input.index).toISOString();
|
|
9047
9682
|
const output = normalizeContent(input.sourceMessage.content);
|
|
9048
9683
|
const parsedOutput = parseJsonValue(output);
|
|
9049
|
-
const toolCallId =
|
|
9050
|
-
const toolName =
|
|
9684
|
+
const toolCallId = readString9(input.sourceMessage, "tool_call_id") ?? input.pending?.toolCall.id;
|
|
9685
|
+
const toolName = readString9(input.sourceMessage, "tool_name") ?? input.pending?.toolCall.name ?? "tool";
|
|
9051
9686
|
return projectHermesAgentEvent({
|
|
9052
9687
|
conversationId: input.conversationId,
|
|
9053
9688
|
messageId: input.messageId,
|
|
@@ -9259,10 +9894,10 @@ function rememberKnownHermesConversation(map, sessionId, conversationId) {
|
|
|
9259
9894
|
}
|
|
9260
9895
|
async function discoverHermesProfileNames() {
|
|
9261
9896
|
const names = /* @__PURE__ */ new Set([DEFAULT_PROFILE_NAME]);
|
|
9262
|
-
const profilesDir =
|
|
9263
|
-
const entries = await
|
|
9897
|
+
const profilesDir = path14.join(os4.homedir(), ".hermes", "profiles");
|
|
9898
|
+
const entries = await readdir6(profilesDir, { withFileTypes: true }).catch(
|
|
9264
9899
|
(error) => {
|
|
9265
|
-
if (
|
|
9900
|
+
if (isNodeError10(error, "ENOENT")) {
|
|
9266
9901
|
return [];
|
|
9267
9902
|
}
|
|
9268
9903
|
throw error;
|
|
@@ -9289,11 +9924,8 @@ async function listProfileSessions(dbPath) {
|
|
|
9289
9924
|
}
|
|
9290
9925
|
let db = null;
|
|
9291
9926
|
try {
|
|
9292
|
-
|
|
9293
|
-
|
|
9294
|
-
);
|
|
9295
|
-
db = new DatabaseSync(dbPath, {
|
|
9296
|
-
readOnly: true,
|
|
9927
|
+
db = openSqliteDatabase(dbPath, {
|
|
9928
|
+
readonly: true,
|
|
9297
9929
|
timeout: 1e3
|
|
9298
9930
|
});
|
|
9299
9931
|
const sessionColumns = readTableColumns(db, "sessions");
|
|
@@ -9325,683 +9957,458 @@ function appendHermesRawMessage(message, row) {
|
|
|
9325
9957
|
format: "hermes-message",
|
|
9326
9958
|
payload: row
|
|
9327
9959
|
} : {
|
|
9328
|
-
format: "hermes-message-group",
|
|
9329
|
-
payload: { messages: [...rows, row] }
|
|
9330
|
-
};
|
|
9331
|
-
}
|
|
9332
|
-
function readHermesRawMessageRows(raw) {
|
|
9333
|
-
if (!raw) {
|
|
9334
|
-
return [];
|
|
9335
|
-
}
|
|
9336
|
-
if (raw.format === "hermes-message-group") {
|
|
9337
|
-
const payload = toRecord7(raw.payload);
|
|
9338
|
-
return Array.isArray(payload.messages) ? payload.messages.filter(
|
|
9339
|
-
(item) => typeof item === "object" && item !== null
|
|
9340
|
-
) : [];
|
|
9341
|
-
}
|
|
9342
|
-
if (raw.format === "hermes-message") {
|
|
9343
|
-
return typeof raw.payload === "object" && raw.payload !== null ? [raw.payload] : [];
|
|
9344
|
-
}
|
|
9345
|
-
return [];
|
|
9346
|
-
}
|
|
9347
|
-
function rememberHermesMessageId(message, row) {
|
|
9348
|
-
if (row.id === void 0 || row.id === null) {
|
|
9349
|
-
return;
|
|
9350
|
-
}
|
|
9351
|
-
const existing = Array.isArray(message.hermes?.message_ids) ? message.hermes.message_ids : message.hermes?.message_id === void 0 ? [] : [message.hermes.message_id];
|
|
9352
|
-
const id = row.id;
|
|
9353
|
-
message.hermes = {
|
|
9354
|
-
...message.hermes ?? {},
|
|
9355
|
-
message_ids: existing.includes(id) ? existing : [...existing, id]
|
|
9356
|
-
};
|
|
9357
|
-
}
|
|
9358
|
-
function joinImportedText(left, right) {
|
|
9359
|
-
if (!left) {
|
|
9360
|
-
return right;
|
|
9361
|
-
}
|
|
9362
|
-
if (!right) {
|
|
9363
|
-
return left;
|
|
9364
|
-
}
|
|
9365
|
-
if (/\s$/u.test(left) || /^\s/u.test(right)) {
|
|
9366
|
-
return `${left}${right}`;
|
|
9367
|
-
}
|
|
9368
|
-
return `${left}
|
|
9369
|
-
|
|
9370
|
-
${right}`;
|
|
9371
|
-
}
|
|
9372
|
-
function projectCompressionTips(rows) {
|
|
9373
|
-
const byId = /* @__PURE__ */ new Map();
|
|
9374
|
-
const childrenByParent = /* @__PURE__ */ new Map();
|
|
9375
|
-
for (const row of rows) {
|
|
9376
|
-
const id = readString8(row, "id");
|
|
9377
|
-
if (!id) {
|
|
9378
|
-
continue;
|
|
9379
|
-
}
|
|
9380
|
-
byId.set(id, row);
|
|
9381
|
-
const parentId = readString8(row, "parent_session_id");
|
|
9382
|
-
if (parentId) {
|
|
9383
|
-
const children = childrenByParent.get(parentId) ?? [];
|
|
9384
|
-
children.push(row);
|
|
9385
|
-
childrenByParent.set(parentId, children);
|
|
9386
|
-
}
|
|
9387
|
-
}
|
|
9388
|
-
const projected = [];
|
|
9389
|
-
for (const row of rows) {
|
|
9390
|
-
const id = readString8(row, "id");
|
|
9391
|
-
if (!id || readString8(row, "parent_session_id")) {
|
|
9392
|
-
continue;
|
|
9393
|
-
}
|
|
9394
|
-
let tip = row;
|
|
9395
|
-
const visited = /* @__PURE__ */ new Set([id]);
|
|
9396
|
-
while (readString8(tip, "end_reason") === "compression") {
|
|
9397
|
-
const tipId2 = readString8(tip, "id");
|
|
9398
|
-
if (!tipId2) {
|
|
9399
|
-
break;
|
|
9400
|
-
}
|
|
9401
|
-
const next = (childrenByParent.get(tipId2) ?? []).filter((child) => readString8(child, "id")).sort(
|
|
9402
|
-
(left, right) => (readNumber2(right.last_active) ?? 0) - (readNumber2(left.last_active) ?? 0)
|
|
9403
|
-
)[0];
|
|
9404
|
-
const nextId = next ? readString8(next, "id") : null;
|
|
9405
|
-
if (!next || !nextId || visited.has(nextId)) {
|
|
9406
|
-
break;
|
|
9407
|
-
}
|
|
9408
|
-
tip = next;
|
|
9409
|
-
visited.add(nextId);
|
|
9410
|
-
}
|
|
9411
|
-
const tipId = readString8(tip, "id");
|
|
9412
|
-
if (tipId) {
|
|
9413
|
-
projected.push({
|
|
9414
|
-
...tip,
|
|
9415
|
-
id: tipId,
|
|
9416
|
-
_lineage_root_id: id,
|
|
9417
|
-
started_at: readNumber2(row.started_at) ?? readNumber2(tip.started_at)
|
|
9418
|
-
});
|
|
9419
|
-
}
|
|
9420
|
-
}
|
|
9421
|
-
return projected;
|
|
9422
|
-
}
|
|
9423
|
-
async function readHermesSessionMessages(candidate) {
|
|
9424
|
-
const [dbMessages, jsonlMessages] = await Promise.all([
|
|
9425
|
-
readStateDbMessages(candidate.dbPath, candidate.session.id),
|
|
9426
|
-
readJsonlMessages(candidate.profileName, candidate.session.id)
|
|
9427
|
-
]);
|
|
9428
|
-
return jsonlMessages.length > dbMessages.length ? jsonlMessages : dbMessages;
|
|
9429
|
-
}
|
|
9430
|
-
async function readStateDbMessages(dbPath, sessionId) {
|
|
9431
|
-
if (!await isFile(dbPath)) {
|
|
9432
|
-
return [];
|
|
9433
|
-
}
|
|
9434
|
-
let db = null;
|
|
9435
|
-
try {
|
|
9436
|
-
const { DatabaseSync } = nodeRequire3(
|
|
9437
|
-
"node:sqlite"
|
|
9438
|
-
);
|
|
9439
|
-
db = new DatabaseSync(dbPath, {
|
|
9440
|
-
readOnly: true,
|
|
9441
|
-
timeout: 1e3
|
|
9442
|
-
});
|
|
9443
|
-
const columns = readTableColumns(db, "messages");
|
|
9444
|
-
if (!columns.has("session_id") || !columns.has("role")) {
|
|
9445
|
-
return [];
|
|
9446
|
-
}
|
|
9447
|
-
const selectColumns = MESSAGE_COLUMNS.map(
|
|
9448
|
-
(column) => columns.has(column) ? quoteIdentifier(column) : `NULL AS ${column}`
|
|
9449
|
-
).join(", ");
|
|
9450
|
-
return db.prepare(
|
|
9451
|
-
`
|
|
9452
|
-
SELECT ${selectColumns}
|
|
9453
|
-
FROM messages
|
|
9454
|
-
WHERE session_id = ?
|
|
9455
|
-
ORDER BY timestamp, id
|
|
9456
|
-
`
|
|
9457
|
-
).all(sessionId);
|
|
9458
|
-
} catch {
|
|
9459
|
-
return [];
|
|
9460
|
-
} finally {
|
|
9461
|
-
db?.close();
|
|
9462
|
-
}
|
|
9463
|
-
}
|
|
9464
|
-
async function readJsonlMessages(profileName, sessionId) {
|
|
9465
|
-
if (!/^[A-Za-z0-9._:-]{1,160}$/u.test(sessionId)) {
|
|
9466
|
-
return [];
|
|
9467
|
-
}
|
|
9468
|
-
const profileDir = resolveHermesProfileDir(profileName);
|
|
9469
|
-
const sessionsDir = await readHermesSessionsDir(profileName).then((value) => value.sessionsDir).catch(() => path13.join(profileDir, "sessions"));
|
|
9470
|
-
const transcriptPath = path13.join(sessionsDir, `${sessionId}.jsonl`);
|
|
9471
|
-
const raw = await readFile8(transcriptPath, "utf8").catch((error) => {
|
|
9472
|
-
if (isNodeError9(error, "ENOENT")) {
|
|
9473
|
-
return "";
|
|
9474
|
-
}
|
|
9475
|
-
throw error;
|
|
9476
|
-
});
|
|
9477
|
-
if (!raw.trim()) {
|
|
9478
|
-
return [];
|
|
9479
|
-
}
|
|
9480
|
-
const rows = [];
|
|
9481
|
-
for (const line of raw.split(/\r?\n/u)) {
|
|
9482
|
-
if (!line.trim()) {
|
|
9483
|
-
continue;
|
|
9484
|
-
}
|
|
9485
|
-
try {
|
|
9486
|
-
const parsed = JSON.parse(line);
|
|
9487
|
-
const normalized = normalizeJsonlMessage(parsed);
|
|
9488
|
-
if (normalized) {
|
|
9489
|
-
rows.push(normalized);
|
|
9490
|
-
}
|
|
9491
|
-
} catch {
|
|
9492
|
-
continue;
|
|
9493
|
-
}
|
|
9494
|
-
}
|
|
9495
|
-
return rows;
|
|
9496
|
-
}
|
|
9497
|
-
function normalizeJsonlMessage(row) {
|
|
9498
|
-
const role = readString8(row, "role");
|
|
9499
|
-
if (!role) {
|
|
9500
|
-
return null;
|
|
9501
|
-
}
|
|
9502
|
-
const content = normalizeContent(row.content);
|
|
9503
|
-
const timestamp = readNumber2(row.timestamp) ?? readNumber2(row.created_at) ?? readNumber2(row.createdAt);
|
|
9504
|
-
return {
|
|
9505
|
-
...row,
|
|
9506
|
-
role,
|
|
9507
|
-
content,
|
|
9508
|
-
timestamp: timestamp ?? void 0
|
|
9509
|
-
};
|
|
9510
|
-
}
|
|
9511
|
-
function toLinkMessage(input) {
|
|
9512
|
-
const role = normalizeMessageRole(input.message.role);
|
|
9513
|
-
const text = normalizeContent(input.message.content);
|
|
9514
|
-
const createdAt = isoFromHermesTime(input.message.timestamp) ?? new Date(Date.now() + input.index).toISOString();
|
|
9515
|
-
return {
|
|
9516
|
-
id: `msg_${randomUUID6().replaceAll("-", "")}`,
|
|
9517
|
-
schema_version: 1,
|
|
9518
|
-
conversation_id: input.conversationId,
|
|
9519
|
-
role,
|
|
9520
|
-
status: "completed",
|
|
9521
|
-
created_at: createdAt,
|
|
9522
|
-
updated_at: createdAt,
|
|
9523
|
-
sender: senderForRole({
|
|
9524
|
-
role,
|
|
9525
|
-
profileName: input.profileName,
|
|
9526
|
-
profileUid: input.profileUid,
|
|
9527
|
-
profileDisplayName: input.profileDisplayName
|
|
9528
|
-
}),
|
|
9529
|
-
parts: text ? [{ type: "text", text }] : [],
|
|
9530
|
-
attachments: [],
|
|
9531
|
-
hermes: {
|
|
9532
|
-
session_id: input.sessionId,
|
|
9533
|
-
message_id: input.message.id,
|
|
9534
|
-
imported_from: "hermes",
|
|
9535
|
-
import_projection: HERMES_IMPORT_PROJECTION_VERSION
|
|
9536
|
-
},
|
|
9537
|
-
raw: {
|
|
9538
|
-
format: "hermes-message",
|
|
9539
|
-
payload: input.message
|
|
9540
|
-
}
|
|
9541
|
-
};
|
|
9542
|
-
}
|
|
9543
|
-
function senderForRole(input) {
|
|
9544
|
-
switch (input.role) {
|
|
9545
|
-
case "user":
|
|
9546
|
-
return { id: "hermes_user", type: "human", display_name: "Me" };
|
|
9547
|
-
case "assistant":
|
|
9548
|
-
return {
|
|
9549
|
-
id: `agent_${input.profileName}`,
|
|
9550
|
-
type: "agent",
|
|
9551
|
-
display_name: input.profileDisplayName,
|
|
9552
|
-
profile_uid: input.profileUid,
|
|
9553
|
-
profile: input.profileName
|
|
9554
|
-
};
|
|
9555
|
-
case "tool":
|
|
9556
|
-
return { id: "hermes_tool", type: "tool", display_name: "Tool" };
|
|
9557
|
-
case "system":
|
|
9558
|
-
return { id: "hermes_system", type: "system", display_name: "System" };
|
|
9559
|
-
}
|
|
9560
|
-
}
|
|
9561
|
-
function firstUserText(snapshot) {
|
|
9562
|
-
return snapshot.messages.find((message) => message.role === "user")?.parts.find((part) => part.type === "text")?.text?.slice(0, 80);
|
|
9563
|
-
}
|
|
9564
|
-
function normalizeTitle(value) {
|
|
9565
|
-
const normalized = value?.replace(/\s+/gu, " ").trim();
|
|
9566
|
-
return normalized || DEFAULT_CONVERSATION_TITLE;
|
|
9567
|
-
}
|
|
9568
|
-
function normalizeMessageRole(value) {
|
|
9569
|
-
switch (value?.trim().toLowerCase()) {
|
|
9570
|
-
case "user":
|
|
9571
|
-
return "user";
|
|
9572
|
-
case "assistant":
|
|
9573
|
-
return "assistant";
|
|
9574
|
-
case "tool":
|
|
9575
|
-
return "tool";
|
|
9576
|
-
case "system":
|
|
9577
|
-
return "system";
|
|
9578
|
-
default:
|
|
9579
|
-
return "system";
|
|
9580
|
-
}
|
|
9581
|
-
}
|
|
9582
|
-
function normalizeContent(value) {
|
|
9583
|
-
if (typeof value === "string") {
|
|
9584
|
-
return value;
|
|
9585
|
-
}
|
|
9586
|
-
if (Array.isArray(value)) {
|
|
9587
|
-
return value.map((item) => {
|
|
9588
|
-
if (typeof item === "string") {
|
|
9589
|
-
return item;
|
|
9590
|
-
}
|
|
9591
|
-
if (typeof item === "object" && item !== null) {
|
|
9592
|
-
return readString8(item, "text") ?? "";
|
|
9593
|
-
}
|
|
9594
|
-
return "";
|
|
9595
|
-
}).filter(Boolean).join("");
|
|
9596
|
-
}
|
|
9597
|
-
return "";
|
|
9960
|
+
format: "hermes-message-group",
|
|
9961
|
+
payload: { messages: [...rows, row] }
|
|
9962
|
+
};
|
|
9598
9963
|
}
|
|
9599
|
-
function
|
|
9600
|
-
if (
|
|
9601
|
-
return
|
|
9964
|
+
function readHermesRawMessageRows(raw) {
|
|
9965
|
+
if (!raw) {
|
|
9966
|
+
return [];
|
|
9602
9967
|
}
|
|
9603
|
-
|
|
9604
|
-
|
|
9605
|
-
return
|
|
9968
|
+
if (raw.format === "hermes-message-group") {
|
|
9969
|
+
const payload = toRecord8(raw.payload);
|
|
9970
|
+
return Array.isArray(payload.messages) ? payload.messages.filter(
|
|
9971
|
+
(item) => typeof item === "object" && item !== null
|
|
9972
|
+
) : [];
|
|
9606
9973
|
}
|
|
9607
|
-
|
|
9608
|
-
return
|
|
9609
|
-
} catch {
|
|
9610
|
-
return void 0;
|
|
9974
|
+
if (raw.format === "hermes-message") {
|
|
9975
|
+
return typeof raw.payload === "object" && raw.payload !== null ? [raw.payload] : [];
|
|
9611
9976
|
}
|
|
9977
|
+
return [];
|
|
9612
9978
|
}
|
|
9613
|
-
function
|
|
9614
|
-
|
|
9615
|
-
|
|
9616
|
-
function isDeletedSession(session) {
|
|
9617
|
-
return readBoolean(session.deleted) || readBoolean(session.is_deleted) || Boolean(readString8(session, "deleted_at")) || ["deleted", "removed"].includes(readString8(session, "status") ?? "");
|
|
9618
|
-
}
|
|
9619
|
-
function isHiddenSession(session) {
|
|
9620
|
-
const source = readString8(session, "source")?.toLowerCase();
|
|
9621
|
-
const status = readString8(session, "status")?.toLowerCase();
|
|
9622
|
-
const visibility = readString8(session, "visibility")?.toLowerCase();
|
|
9623
|
-
return Boolean(source && HIDDEN_SESSION_SOURCES.has(source)) || readBoolean(session.hidden) || readBoolean(session.archived) || Boolean(readString8(session, "archived_at")) || status === "hidden" || status === "archived" || visibility === "hidden" || visibility === "hide";
|
|
9624
|
-
}
|
|
9625
|
-
function readTableColumns(db, tableName) {
|
|
9626
|
-
try {
|
|
9627
|
-
const rows = db.prepare(`PRAGMA table_info(${quoteIdentifier(tableName)})`).all();
|
|
9628
|
-
return new Set(
|
|
9629
|
-
rows.map((row) => typeof row.name === "string" ? row.name : "").filter(Boolean)
|
|
9630
|
-
);
|
|
9631
|
-
} catch {
|
|
9632
|
-
return /* @__PURE__ */ new Set();
|
|
9979
|
+
function rememberHermesMessageId(message, row) {
|
|
9980
|
+
if (row.id === void 0 || row.id === null) {
|
|
9981
|
+
return;
|
|
9633
9982
|
}
|
|
9983
|
+
const existing = Array.isArray(message.hermes?.message_ids) ? message.hermes.message_ids : message.hermes?.message_id === void 0 ? [] : [message.hermes.message_id];
|
|
9984
|
+
const id = row.id;
|
|
9985
|
+
message.hermes = {
|
|
9986
|
+
...message.hermes ?? {},
|
|
9987
|
+
message_ids: existing.includes(id) ? existing : [...existing, id]
|
|
9988
|
+
};
|
|
9634
9989
|
}
|
|
9635
|
-
function
|
|
9636
|
-
|
|
9637
|
-
|
|
9638
|
-
async function isFile(filePath) {
|
|
9639
|
-
return stat7(filePath).then((value) => value.isFile()).catch((error) => {
|
|
9640
|
-
if (isNodeError9(error, "ENOENT")) {
|
|
9641
|
-
return false;
|
|
9642
|
-
}
|
|
9643
|
-
throw error;
|
|
9644
|
-
});
|
|
9645
|
-
}
|
|
9646
|
-
function createConversationId() {
|
|
9647
|
-
return `conv_${randomUUID6().replaceAll("-", "")}`;
|
|
9648
|
-
}
|
|
9649
|
-
function isoFromHermesTime(value) {
|
|
9650
|
-
const numeric = readNumber2(value);
|
|
9651
|
-
if (!numeric || numeric <= 0) {
|
|
9652
|
-
return void 0;
|
|
9990
|
+
function joinImportedText(left, right) {
|
|
9991
|
+
if (!left) {
|
|
9992
|
+
return right;
|
|
9653
9993
|
}
|
|
9654
|
-
|
|
9655
|
-
|
|
9656
|
-
}
|
|
9657
|
-
|
|
9658
|
-
|
|
9659
|
-
|
|
9660
|
-
}
|
|
9661
|
-
|
|
9662
|
-
|
|
9994
|
+
if (!right) {
|
|
9995
|
+
return left;
|
|
9996
|
+
}
|
|
9997
|
+
if (/\s$/u.test(left) || /^\s/u.test(right)) {
|
|
9998
|
+
return `${left}${right}`;
|
|
9999
|
+
}
|
|
10000
|
+
return `${left}
|
|
10001
|
+
|
|
10002
|
+
${right}`;
|
|
9663
10003
|
}
|
|
9664
|
-
function
|
|
9665
|
-
|
|
9666
|
-
|
|
10004
|
+
function projectCompressionTips(rows) {
|
|
10005
|
+
const byId = /* @__PURE__ */ new Map();
|
|
10006
|
+
const childrenByParent = /* @__PURE__ */ new Map();
|
|
10007
|
+
for (const row of rows) {
|
|
10008
|
+
const id = readString9(row, "id");
|
|
10009
|
+
if (!id) {
|
|
10010
|
+
continue;
|
|
10011
|
+
}
|
|
10012
|
+
byId.set(id, row);
|
|
10013
|
+
const parentId = readString9(row, "parent_session_id");
|
|
10014
|
+
if (parentId) {
|
|
10015
|
+
const children = childrenByParent.get(parentId) ?? [];
|
|
10016
|
+
children.push(row);
|
|
10017
|
+
childrenByParent.set(parentId, children);
|
|
10018
|
+
}
|
|
9667
10019
|
}
|
|
9668
|
-
|
|
9669
|
-
|
|
10020
|
+
const projected = [];
|
|
10021
|
+
for (const row of rows) {
|
|
10022
|
+
const id = readString9(row, "id");
|
|
10023
|
+
if (!id || readString9(row, "parent_session_id")) {
|
|
10024
|
+
continue;
|
|
10025
|
+
}
|
|
10026
|
+
let tip = row;
|
|
10027
|
+
const visited = /* @__PURE__ */ new Set([id]);
|
|
10028
|
+
while (readString9(tip, "end_reason") === "compression") {
|
|
10029
|
+
const tipId2 = readString9(tip, "id");
|
|
10030
|
+
if (!tipId2) {
|
|
10031
|
+
break;
|
|
10032
|
+
}
|
|
10033
|
+
const next = (childrenByParent.get(tipId2) ?? []).filter((child) => readString9(child, "id")).sort(
|
|
10034
|
+
(left, right) => (readNumber2(right.last_active) ?? 0) - (readNumber2(left.last_active) ?? 0)
|
|
10035
|
+
)[0];
|
|
10036
|
+
const nextId = next ? readString9(next, "id") : null;
|
|
10037
|
+
if (!next || !nextId || visited.has(nextId)) {
|
|
10038
|
+
break;
|
|
10039
|
+
}
|
|
10040
|
+
tip = next;
|
|
10041
|
+
visited.add(nextId);
|
|
10042
|
+
}
|
|
10043
|
+
const tipId = readString9(tip, "id");
|
|
10044
|
+
if (tipId) {
|
|
10045
|
+
projected.push({
|
|
10046
|
+
...tip,
|
|
10047
|
+
id: tipId,
|
|
10048
|
+
_lineage_root_id: id,
|
|
10049
|
+
started_at: readNumber2(row.started_at) ?? readNumber2(tip.started_at)
|
|
10050
|
+
});
|
|
10051
|
+
}
|
|
9670
10052
|
}
|
|
9671
|
-
return
|
|
10053
|
+
return projected;
|
|
9672
10054
|
}
|
|
9673
|
-
function
|
|
9674
|
-
|
|
10055
|
+
async function readHermesSessionMessages(candidate) {
|
|
10056
|
+
const [dbMessages, jsonlMessages] = await Promise.all([
|
|
10057
|
+
readStateDbMessages(candidate.dbPath, candidate.session.id),
|
|
10058
|
+
readJsonlMessages(candidate.profileName, candidate.session.id)
|
|
10059
|
+
]);
|
|
10060
|
+
return jsonlMessages.length > dbMessages.length ? jsonlMessages : dbMessages;
|
|
9675
10061
|
}
|
|
9676
|
-
|
|
9677
|
-
|
|
9678
|
-
|
|
9679
|
-
import path14 from "path";
|
|
9680
|
-
var MAX_IMPORTED_BLOB_BYTES = 100 * 1024 * 1024;
|
|
9681
|
-
var MAX_MEDIA_IMPORT_FAILURES = 20;
|
|
9682
|
-
var MAX_DELIVERY_FILES = 50;
|
|
9683
|
-
var DELIVERY_STAGING_SEGMENT = "delivery-staging";
|
|
9684
|
-
var SUPPORTED_DELIVERY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
9685
|
-
".png",
|
|
9686
|
-
".jpg",
|
|
9687
|
-
".jpeg",
|
|
9688
|
-
".gif",
|
|
9689
|
-
".webp",
|
|
9690
|
-
".heic",
|
|
9691
|
-
".pdf",
|
|
9692
|
-
".txt",
|
|
9693
|
-
".log",
|
|
9694
|
-
".md",
|
|
9695
|
-
".markdown",
|
|
9696
|
-
".json",
|
|
9697
|
-
".jsonl",
|
|
9698
|
-
".yaml",
|
|
9699
|
-
".yml",
|
|
9700
|
-
".toml",
|
|
9701
|
-
".ini",
|
|
9702
|
-
".xml",
|
|
9703
|
-
".html",
|
|
9704
|
-
".css",
|
|
9705
|
-
".js",
|
|
9706
|
-
".ts",
|
|
9707
|
-
".jsx",
|
|
9708
|
-
".tsx",
|
|
9709
|
-
".dart",
|
|
9710
|
-
".py",
|
|
9711
|
-
".java",
|
|
9712
|
-
".kt",
|
|
9713
|
-
".swift",
|
|
9714
|
-
".go",
|
|
9715
|
-
".rs",
|
|
9716
|
-
".rb",
|
|
9717
|
-
".php",
|
|
9718
|
-
".c",
|
|
9719
|
-
".cc",
|
|
9720
|
-
".cpp",
|
|
9721
|
-
".h",
|
|
9722
|
-
".hpp",
|
|
9723
|
-
".cs",
|
|
9724
|
-
".sql",
|
|
9725
|
-
".csv",
|
|
9726
|
-
".tsv",
|
|
9727
|
-
".doc",
|
|
9728
|
-
".docx",
|
|
9729
|
-
".xls",
|
|
9730
|
-
".xlsx",
|
|
9731
|
-
".ppt",
|
|
9732
|
-
".pptx",
|
|
9733
|
-
".zip",
|
|
9734
|
-
".rar",
|
|
9735
|
-
".7z",
|
|
9736
|
-
".tar",
|
|
9737
|
-
".gz",
|
|
9738
|
-
".mp4",
|
|
9739
|
-
".mov",
|
|
9740
|
-
".avi",
|
|
9741
|
-
".mkv",
|
|
9742
|
-
".webm",
|
|
9743
|
-
".ogg",
|
|
9744
|
-
".opus",
|
|
9745
|
-
".mp3",
|
|
9746
|
-
".wav",
|
|
9747
|
-
".m4a"
|
|
9748
|
-
]);
|
|
9749
|
-
function resolveDeliveryStagingTarget(paths, stagingDir) {
|
|
9750
|
-
const resolvedDir = path14.resolve(stagingDir);
|
|
9751
|
-
const relative = path14.relative(path14.resolve(paths.conversationsDir), resolvedDir);
|
|
9752
|
-
if (!relative || relative.startsWith("..") || path14.isAbsolute(relative)) {
|
|
9753
|
-
throw new LinkHttpError(
|
|
9754
|
-
400,
|
|
9755
|
-
"delivery_staging_invalid",
|
|
9756
|
-
"delivery staging directory must be inside Hermes Link conversations"
|
|
9757
|
-
);
|
|
10062
|
+
async function readStateDbMessages(dbPath, sessionId) {
|
|
10063
|
+
if (!await isFile(dbPath)) {
|
|
10064
|
+
return [];
|
|
9758
10065
|
}
|
|
9759
|
-
|
|
9760
|
-
|
|
9761
|
-
|
|
9762
|
-
|
|
9763
|
-
|
|
9764
|
-
|
|
9765
|
-
);
|
|
10066
|
+
let db = null;
|
|
10067
|
+
try {
|
|
10068
|
+
db = openSqliteDatabase(dbPath, {
|
|
10069
|
+
readonly: true,
|
|
10070
|
+
timeout: 1e3
|
|
10071
|
+
});
|
|
10072
|
+
const columns = readTableColumns(db, "messages");
|
|
10073
|
+
if (!columns.has("session_id") || !columns.has("role")) {
|
|
10074
|
+
return [];
|
|
10075
|
+
}
|
|
10076
|
+
const selectColumns = MESSAGE_COLUMNS.map(
|
|
10077
|
+
(column) => columns.has(column) ? quoteIdentifier(column) : `NULL AS ${column}`
|
|
10078
|
+
).join(", ");
|
|
10079
|
+
return db.prepare(
|
|
10080
|
+
`
|
|
10081
|
+
SELECT ${selectColumns}
|
|
10082
|
+
FROM messages
|
|
10083
|
+
WHERE session_id = ?
|
|
10084
|
+
ORDER BY timestamp, id
|
|
10085
|
+
`
|
|
10086
|
+
).all(sessionId);
|
|
10087
|
+
} catch {
|
|
10088
|
+
return [];
|
|
10089
|
+
} finally {
|
|
10090
|
+
db?.close();
|
|
9766
10091
|
}
|
|
9767
|
-
|
|
9768
|
-
|
|
9769
|
-
|
|
9770
|
-
|
|
9771
|
-
}
|
|
9772
|
-
|
|
9773
|
-
|
|
9774
|
-
const
|
|
10092
|
+
}
|
|
10093
|
+
async function readJsonlMessages(profileName, sessionId) {
|
|
10094
|
+
if (!/^[A-Za-z0-9._:-]{1,160}$/u.test(sessionId)) {
|
|
10095
|
+
return [];
|
|
10096
|
+
}
|
|
10097
|
+
const profileDir = resolveHermesProfileDir(profileName);
|
|
10098
|
+
const sessionsDir = await readHermesSessionsDir(profileName).then((value) => value.sessionsDir).catch(() => path14.join(profileDir, "sessions"));
|
|
10099
|
+
const transcriptPath = path14.join(sessionsDir, `${sessionId}.jsonl`);
|
|
10100
|
+
const raw = await readFile9(transcriptPath, "utf8").catch((error) => {
|
|
9775
10101
|
if (isNodeError10(error, "ENOENT")) {
|
|
9776
|
-
|
|
9777
|
-
404,
|
|
9778
|
-
"delivery_staging_not_found",
|
|
9779
|
-
"delivery staging directory was not found"
|
|
9780
|
-
);
|
|
10102
|
+
return "";
|
|
9781
10103
|
}
|
|
9782
10104
|
throw error;
|
|
9783
10105
|
});
|
|
9784
|
-
if (!
|
|
9785
|
-
|
|
9786
|
-
400,
|
|
9787
|
-
"delivery_staging_not_directory",
|
|
9788
|
-
"delivery staging path is not a directory"
|
|
9789
|
-
);
|
|
10106
|
+
if (!raw.trim()) {
|
|
10107
|
+
return [];
|
|
9790
10108
|
}
|
|
9791
|
-
const
|
|
9792
|
-
|
|
9793
|
-
(
|
|
9794
|
-
|
|
9795
|
-
|
|
9796
|
-
|
|
9797
|
-
|
|
9798
|
-
|
|
9799
|
-
|
|
9800
|
-
|
|
9801
|
-
|
|
9802
|
-
|
|
10109
|
+
const rows = [];
|
|
10110
|
+
for (const line of raw.split(/\r?\n/u)) {
|
|
10111
|
+
if (!line.trim()) {
|
|
10112
|
+
continue;
|
|
10113
|
+
}
|
|
10114
|
+
try {
|
|
10115
|
+
const parsed = JSON.parse(line);
|
|
10116
|
+
const normalized = normalizeJsonlMessage(parsed);
|
|
10117
|
+
if (normalized) {
|
|
10118
|
+
rows.push(normalized);
|
|
10119
|
+
}
|
|
10120
|
+
} catch {
|
|
10121
|
+
continue;
|
|
10122
|
+
}
|
|
10123
|
+
}
|
|
10124
|
+
return rows;
|
|
9803
10125
|
}
|
|
9804
|
-
|
|
9805
|
-
const
|
|
9806
|
-
if (
|
|
9807
|
-
return
|
|
10126
|
+
function normalizeJsonlMessage(row) {
|
|
10127
|
+
const role = readString9(row, "role");
|
|
10128
|
+
if (!role) {
|
|
10129
|
+
return null;
|
|
9808
10130
|
}
|
|
9809
|
-
const
|
|
9810
|
-
const
|
|
9811
|
-
|
|
9812
|
-
|
|
9813
|
-
|
|
9814
|
-
|
|
10131
|
+
const content = normalizeContent(row.content);
|
|
10132
|
+
const timestamp = readNumber2(row.timestamp) ?? readNumber2(row.created_at) ?? readNumber2(row.createdAt);
|
|
10133
|
+
return {
|
|
10134
|
+
...row,
|
|
10135
|
+
role,
|
|
10136
|
+
content,
|
|
10137
|
+
timestamp: timestamp ?? void 0
|
|
10138
|
+
};
|
|
10139
|
+
}
|
|
10140
|
+
async function hydrateImportedConversationMedia(input) {
|
|
10141
|
+
const snapshot = await input.store.readSnapshot(input.conversationId);
|
|
10142
|
+
const imports = snapshot.messages.map((message) => ({
|
|
10143
|
+
messageId: message.id,
|
|
10144
|
+
references: collectMessageMediaReferences(message)
|
|
10145
|
+
})).filter((item) => item.references.length > 0);
|
|
10146
|
+
if (imports.length === 0) {
|
|
10147
|
+
return;
|
|
9815
10148
|
}
|
|
9816
|
-
const
|
|
9817
|
-
const
|
|
9818
|
-
|
|
9819
|
-
|
|
9820
|
-
|
|
9821
|
-
|
|
9822
|
-
|
|
9823
|
-
|
|
9824
|
-
|
|
9825
|
-
|
|
9826
|
-
|
|
9827
|
-
|
|
9828
|
-
|
|
9829
|
-
|
|
9830
|
-
|
|
9831
|
-
|
|
9832
|
-
|
|
9833
|
-
|
|
9834
|
-
|
|
9835
|
-
|
|
9836
|
-
|
|
9837
|
-
|
|
9838
|
-
filename: blob.filename,
|
|
9839
|
-
url: `/api/v1/conversations/${encodeURIComponent(input.conversationId)}/blobs/${encodeURIComponent(blob.id)}`
|
|
9840
|
-
};
|
|
9841
|
-
assistant.parts.push(part);
|
|
9842
|
-
assistant.attachments.push({
|
|
9843
|
-
blob_id: blob.id,
|
|
9844
|
-
mime: blob.mime,
|
|
9845
|
-
size: blob.size,
|
|
9846
|
-
filename: blob.filename,
|
|
9847
|
-
source: "hermes_output"
|
|
9848
|
-
});
|
|
9849
|
-
importedSourceKeys.add(sourceKey);
|
|
9850
|
-
importedParts.push(part);
|
|
9851
|
-
} catch (error) {
|
|
9852
|
-
if (sourceKey && !failedSourceKeys.has(sourceKey)) {
|
|
9853
|
-
const failure = describeMediaImportFailure(reference, sourceKey, error);
|
|
9854
|
-
failedSourceKeys.add(sourceKey);
|
|
9855
|
-
failureRecordsByKey.set(sourceKey, failure);
|
|
9856
|
-
newFailures.push(failure);
|
|
10149
|
+
const outcomes = /* @__PURE__ */ new Map();
|
|
10150
|
+
for (const item of imports) {
|
|
10151
|
+
const result = await importMediaReferencesForMessage(
|
|
10152
|
+
{
|
|
10153
|
+
logger: input.logger,
|
|
10154
|
+
readSnapshot: (conversationId) => input.store.readSnapshot(conversationId),
|
|
10155
|
+
writeSnapshot: (conversationId, nextSnapshot2) => input.store.writeSnapshot(conversationId, nextSnapshot2),
|
|
10156
|
+
appendEvent: async (conversationId, event) => ({
|
|
10157
|
+
seq: 0,
|
|
10158
|
+
conversation_id: conversationId,
|
|
10159
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10160
|
+
...event
|
|
10161
|
+
}),
|
|
10162
|
+
writeBlob: (conversationId, blob) => writeConversationBlob(input.paths, conversationId, blob, {
|
|
10163
|
+
maxBytes: MAX_IMPORTED_HERMES_MEDIA_BYTES
|
|
10164
|
+
})
|
|
10165
|
+
},
|
|
10166
|
+
{
|
|
10167
|
+
conversationId: input.conversationId,
|
|
10168
|
+
runId: IMPORTED_MEDIA_PLACEHOLDER_RUN_ID,
|
|
10169
|
+
messageId: item.messageId,
|
|
10170
|
+
references: item.references
|
|
9857
10171
|
}
|
|
9858
|
-
|
|
9859
|
-
|
|
9860
|
-
|
|
9861
|
-
|
|
9862
|
-
|
|
10172
|
+
);
|
|
10173
|
+
if (result.imported_count > 0 || result.failed_count > 0) {
|
|
10174
|
+
outcomes.set(item.messageId, {
|
|
10175
|
+
imported: result.imported_count > 0,
|
|
10176
|
+
failed: result.failed_count > 0
|
|
9863
10177
|
});
|
|
9864
10178
|
}
|
|
9865
10179
|
}
|
|
9866
|
-
if (
|
|
9867
|
-
return
|
|
9868
|
-
...emptyImportResult(input),
|
|
9869
|
-
discovered_count: references.length,
|
|
9870
|
-
skipped_count: skippedCount
|
|
9871
|
-
};
|
|
10180
|
+
if (outcomes.size === 0) {
|
|
10181
|
+
return;
|
|
9872
10182
|
}
|
|
9873
|
-
|
|
9874
|
-
|
|
9875
|
-
|
|
9876
|
-
|
|
9877
|
-
|
|
9878
|
-
|
|
9879
|
-
|
|
10183
|
+
const nextSnapshot = await input.store.readSnapshot(input.conversationId);
|
|
10184
|
+
let changed = false;
|
|
10185
|
+
for (const message of nextSnapshot.messages) {
|
|
10186
|
+
const outcome = outcomes.get(message.id);
|
|
10187
|
+
if (!outcome) {
|
|
10188
|
+
continue;
|
|
10189
|
+
}
|
|
10190
|
+
if (outcome.imported) {
|
|
10191
|
+
cleanMessageTextParts(message);
|
|
10192
|
+
changed = true;
|
|
10193
|
+
}
|
|
10194
|
+
if (outcome.failed) {
|
|
10195
|
+
changed = appendImportedMediaImportFailureNotice(message) || changed;
|
|
10196
|
+
}
|
|
10197
|
+
}
|
|
10198
|
+
if (changed) {
|
|
10199
|
+
await input.store.writeSnapshot(input.conversationId, nextSnapshot);
|
|
10200
|
+
}
|
|
10201
|
+
}
|
|
10202
|
+
function collectMessageMediaReferences(message) {
|
|
10203
|
+
return message.parts.flatMap(
|
|
10204
|
+
(part) => part.type === "text" && part.text ? collectMediaTags(part.text) : []
|
|
10205
|
+
);
|
|
10206
|
+
}
|
|
10207
|
+
function appendImportedMediaImportFailureNotice(message) {
|
|
10208
|
+
const hermes = toRecord8(message.hermes);
|
|
10209
|
+
if (hermes.media_import_failure_notice_appended === true) {
|
|
10210
|
+
return false;
|
|
10211
|
+
}
|
|
10212
|
+
const failures = readMediaImportFailures(message);
|
|
10213
|
+
if (failures.length === 0) {
|
|
10214
|
+
return false;
|
|
10215
|
+
}
|
|
10216
|
+
const notice = formatImportedMediaImportFailureNotice(failures);
|
|
10217
|
+
const textPart = message.parts.find((part) => part.type === "text");
|
|
10218
|
+
if (textPart) {
|
|
10219
|
+
const currentText = textPart.text ?? "";
|
|
10220
|
+
const separator = currentText.trim().length > 0 ? "\n\n" : "";
|
|
10221
|
+
textPart.text = `${currentText.trimEnd()}${separator}${notice}`;
|
|
10222
|
+
} else {
|
|
10223
|
+
message.parts.unshift({ type: "text", text: notice });
|
|
10224
|
+
}
|
|
10225
|
+
message.hermes = {
|
|
10226
|
+
...hermes,
|
|
10227
|
+
media_import_failure_notice_appended: true
|
|
9880
10228
|
};
|
|
9881
|
-
|
|
9882
|
-
|
|
9883
|
-
|
|
9884
|
-
|
|
9885
|
-
|
|
9886
|
-
|
|
9887
|
-
|
|
9888
|
-
|
|
9889
|
-
|
|
9890
|
-
|
|
9891
|
-
|
|
10229
|
+
return true;
|
|
10230
|
+
}
|
|
10231
|
+
function formatImportedMediaImportFailureNotice(failures) {
|
|
10232
|
+
const filenames = failures.map((failure) => failure.filename);
|
|
10233
|
+
const target = filenames.length === 1 ? `\u6587\u4EF6\u201C${filenames[0]}\u201D` : `${filenames.length} \u4E2A\u6587\u4EF6\uFF08${formatImportedFilenameList(filenames)}\uFF09`;
|
|
10234
|
+
const permissionDenied = failures.some((failure) => {
|
|
10235
|
+
const code = failure.code?.toUpperCase();
|
|
10236
|
+
return code === "EPERM" || code === "EACCES" || /operation not permitted|permission denied/iu.test(failure.reason);
|
|
10237
|
+
});
|
|
10238
|
+
if (permissionDenied) {
|
|
10239
|
+
return `${target}\u6CA1\u80FD\u4F5C\u4E3A\u9644\u4EF6\u5BFC\u5165\uFF1AHermes Link \u8BFB\u53D6\u6587\u4EF6\u65F6\u88AB macOS \u62D2\u7EDD\u4E86\u3002\u8BF7\u5728\u201C\u7CFB\u7EDF\u8BBE\u7F6E > \u9690\u79C1\u4E0E\u5B89\u5168\u6027 > \u5B8C\u5168\u78C1\u76D8\u8BBF\u95EE\u6743\u9650\u201D\u91CC\u7ED9\u8FD0\u884C Link \u7684\u7EC8\u7AEF\u6216 Node \u6388\u6743\u540E\u91CD\u8BD5\u3002`;
|
|
10240
|
+
}
|
|
10241
|
+
return `${target}\u6CA1\u80FD\u4F5C\u4E3A\u9644\u4EF6\u5BFC\u5165\uFF1AHermes Link \u8BFB\u53D6\u672C\u673A\u6587\u4EF6\u5931\u8D25\u4E86\uFF0C\u8BF7\u786E\u8BA4\u6587\u4EF6\u8FD8\u5728\u539F\u4F4D\u7F6E\u5E76\u7A0D\u540E\u91CD\u8BD5\u3002`;
|
|
10242
|
+
}
|
|
10243
|
+
function formatImportedFilenameList(filenames) {
|
|
10244
|
+
const preview = filenames.slice(0, 3).map((filename) => `\u201C${filename}\u201D`);
|
|
10245
|
+
const remaining = filenames.length - preview.length;
|
|
10246
|
+
return remaining > 0 ? `${preview.join("\u3001")} \u7B49 ${filenames.length} \u4E2A` : preview.join("\u3001");
|
|
10247
|
+
}
|
|
10248
|
+
function toLinkMessage(input) {
|
|
10249
|
+
const role = normalizeMessageRole(input.message.role);
|
|
10250
|
+
const text = normalizeContent(input.message.content);
|
|
10251
|
+
const createdAt = isoFromHermesTime(input.message.timestamp) ?? new Date(Date.now() + input.index).toISOString();
|
|
10252
|
+
return {
|
|
10253
|
+
id: `msg_${randomUUID6().replaceAll("-", "")}`,
|
|
10254
|
+
schema_version: 1,
|
|
10255
|
+
conversation_id: input.conversationId,
|
|
10256
|
+
role,
|
|
10257
|
+
status: "completed",
|
|
10258
|
+
created_at: createdAt,
|
|
10259
|
+
updated_at: createdAt,
|
|
10260
|
+
sender: senderForRole({
|
|
10261
|
+
role,
|
|
10262
|
+
profileName: input.profileName,
|
|
10263
|
+
profileUid: input.profileUid,
|
|
10264
|
+
profileDisplayName: input.profileDisplayName
|
|
10265
|
+
}),
|
|
10266
|
+
parts: text ? [{ type: "text", text }] : [],
|
|
10267
|
+
attachments: [],
|
|
10268
|
+
hermes: {
|
|
10269
|
+
session_id: input.sessionId,
|
|
10270
|
+
message_id: input.message.id,
|
|
10271
|
+
imported_from: "hermes",
|
|
10272
|
+
import_projection: HERMES_IMPORT_PROJECTION_VERSION
|
|
10273
|
+
},
|
|
10274
|
+
raw: {
|
|
10275
|
+
format: "hermes-message",
|
|
10276
|
+
payload: input.message
|
|
10277
|
+
}
|
|
10278
|
+
};
|
|
10279
|
+
}
|
|
10280
|
+
function senderForRole(input) {
|
|
10281
|
+
switch (input.role) {
|
|
10282
|
+
case "user":
|
|
10283
|
+
return { id: "hermes_user", type: "human", display_name: "Me" };
|
|
10284
|
+
case "assistant":
|
|
10285
|
+
return {
|
|
10286
|
+
id: `agent_${input.profileName}`,
|
|
10287
|
+
type: "agent",
|
|
10288
|
+
display_name: input.profileDisplayName,
|
|
10289
|
+
profile_uid: input.profileUid,
|
|
10290
|
+
profile: input.profileName
|
|
10291
|
+
};
|
|
10292
|
+
case "tool":
|
|
10293
|
+
return { id: "hermes_tool", type: "tool", display_name: "Tool" };
|
|
10294
|
+
case "system":
|
|
10295
|
+
return { id: "hermes_system", type: "system", display_name: "System" };
|
|
10296
|
+
}
|
|
10297
|
+
}
|
|
10298
|
+
function firstUserText(snapshot) {
|
|
10299
|
+
return snapshot.messages.find((message) => message.role === "user")?.parts.find((part) => part.type === "text")?.text?.slice(0, 80);
|
|
10300
|
+
}
|
|
10301
|
+
function normalizeTitle(value) {
|
|
10302
|
+
const normalized = value?.replace(/\s+/gu, " ").trim();
|
|
10303
|
+
return normalized || DEFAULT_CONVERSATION_TITLE;
|
|
10304
|
+
}
|
|
10305
|
+
function normalizeMessageRole(value) {
|
|
10306
|
+
switch (value?.trim().toLowerCase()) {
|
|
10307
|
+
case "user":
|
|
10308
|
+
return "user";
|
|
10309
|
+
case "assistant":
|
|
10310
|
+
return "assistant";
|
|
10311
|
+
case "tool":
|
|
10312
|
+
return "tool";
|
|
10313
|
+
case "system":
|
|
10314
|
+
return "system";
|
|
10315
|
+
default:
|
|
10316
|
+
return "system";
|
|
9892
10317
|
}
|
|
9893
|
-
return {
|
|
9894
|
-
conversation_id: input.conversationId,
|
|
9895
|
-
run_id: input.runId,
|
|
9896
|
-
message_id: input.messageId,
|
|
9897
|
-
discovered_count: references.length,
|
|
9898
|
-
imported_count: importedParts.length,
|
|
9899
|
-
skipped_count: skippedCount,
|
|
9900
|
-
failed_count: newFailures.length,
|
|
9901
|
-
parts: importedParts,
|
|
9902
|
-
...lastEventSeq ? { last_event_seq: lastEventSeq } : {}
|
|
9903
|
-
};
|
|
9904
10318
|
}
|
|
9905
|
-
function
|
|
9906
|
-
|
|
9907
|
-
|
|
9908
|
-
if (!Array.isArray(failures)) {
|
|
9909
|
-
return [];
|
|
10319
|
+
function normalizeContent(value) {
|
|
10320
|
+
if (typeof value === "string") {
|
|
10321
|
+
return value;
|
|
9910
10322
|
}
|
|
9911
|
-
|
|
9912
|
-
|
|
9913
|
-
|
|
9914
|
-
|
|
9915
|
-
const reason = readString9(record, "reason");
|
|
9916
|
-
if (!key || !filename || !reason) {
|
|
9917
|
-
return [];
|
|
9918
|
-
}
|
|
9919
|
-
return [
|
|
9920
|
-
{
|
|
9921
|
-
key,
|
|
9922
|
-
filename,
|
|
9923
|
-
reason,
|
|
9924
|
-
...readString9(record, "code") ? { code: readString9(record, "code") } : {}
|
|
10323
|
+
if (Array.isArray(value)) {
|
|
10324
|
+
return value.map((item) => {
|
|
10325
|
+
if (typeof item === "string") {
|
|
10326
|
+
return item;
|
|
9925
10327
|
}
|
|
9926
|
-
|
|
9927
|
-
|
|
10328
|
+
if (typeof item === "object" && item !== null) {
|
|
10329
|
+
return readString9(item, "text") ?? "";
|
|
10330
|
+
}
|
|
10331
|
+
return "";
|
|
10332
|
+
}).filter(Boolean).join("");
|
|
10333
|
+
}
|
|
10334
|
+
return "";
|
|
9928
10335
|
}
|
|
9929
|
-
function
|
|
9930
|
-
|
|
9931
|
-
|
|
9932
|
-
|
|
10336
|
+
function parseJsonValue(value) {
|
|
10337
|
+
if (typeof value !== "string") {
|
|
10338
|
+
return void 0;
|
|
10339
|
+
}
|
|
10340
|
+
const trimmed = value.trim();
|
|
10341
|
+
if (!trimmed) {
|
|
10342
|
+
return void 0;
|
|
10343
|
+
}
|
|
10344
|
+
try {
|
|
10345
|
+
return JSON.parse(trimmed);
|
|
10346
|
+
} catch {
|
|
10347
|
+
return void 0;
|
|
10348
|
+
}
|
|
10349
|
+
}
|
|
10350
|
+
function toRecord8(value) {
|
|
10351
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
10352
|
+
}
|
|
10353
|
+
function isDeletedSession(session) {
|
|
10354
|
+
return readBoolean(session.deleted) || readBoolean(session.is_deleted) || Boolean(readString9(session, "deleted_at")) || ["deleted", "removed"].includes(readString9(session, "status") ?? "");
|
|
10355
|
+
}
|
|
10356
|
+
function isHiddenSession(session) {
|
|
10357
|
+
const source = readString9(session, "source")?.toLowerCase();
|
|
10358
|
+
const status = readString9(session, "status")?.toLowerCase();
|
|
10359
|
+
const visibility = readString9(session, "visibility")?.toLowerCase();
|
|
10360
|
+
return Boolean(source && HIDDEN_SESSION_SOURCES.has(source)) || readBoolean(session.hidden) || readBoolean(session.archived) || Boolean(readString9(session, "archived_at")) || status === "hidden" || status === "archived" || visibility === "hidden" || visibility === "hide";
|
|
10361
|
+
}
|
|
10362
|
+
function readTableColumns(db, tableName) {
|
|
10363
|
+
try {
|
|
10364
|
+
const rows = db.prepare(`PRAGMA table_info(${quoteIdentifier(tableName)})`).all();
|
|
10365
|
+
return new Set(
|
|
10366
|
+
rows.map((row) => typeof row.name === "string" ? row.name : "").filter(Boolean)
|
|
10367
|
+
);
|
|
10368
|
+
} catch {
|
|
9933
10369
|
return /* @__PURE__ */ new Set();
|
|
9934
10370
|
}
|
|
9935
|
-
return new Set(
|
|
9936
|
-
keys.filter(
|
|
9937
|
-
(key) => typeof key === "string" && key.length > 0
|
|
9938
|
-
)
|
|
9939
|
-
);
|
|
9940
10371
|
}
|
|
9941
|
-
function
|
|
9942
|
-
return {
|
|
9943
|
-
conversation_id: input.conversationId,
|
|
9944
|
-
run_id: input.runId,
|
|
9945
|
-
message_id: input.messageId,
|
|
9946
|
-
discovered_count: 0,
|
|
9947
|
-
imported_count: 0,
|
|
9948
|
-
skipped_count: 0,
|
|
9949
|
-
failed_count: 0,
|
|
9950
|
-
parts: []
|
|
9951
|
-
};
|
|
10372
|
+
function quoteIdentifier(value) {
|
|
10373
|
+
return `"${value.replaceAll('"', '""')}"`;
|
|
9952
10374
|
}
|
|
9953
|
-
async function
|
|
9954
|
-
|
|
9955
|
-
const fileStat = await stat8(sourcePath).catch((error) => {
|
|
10375
|
+
async function isFile(filePath) {
|
|
10376
|
+
return stat8(filePath).then((value) => value.isFile()).catch((error) => {
|
|
9956
10377
|
if (isNodeError10(error, "ENOENT")) {
|
|
9957
|
-
|
|
9958
|
-
404,
|
|
9959
|
-
"media_source_not_found",
|
|
9960
|
-
"Hermes output file was not found"
|
|
9961
|
-
);
|
|
10378
|
+
return false;
|
|
9962
10379
|
}
|
|
9963
10380
|
throw error;
|
|
9964
10381
|
});
|
|
9965
|
-
if (!fileStat.isFile()) {
|
|
9966
|
-
throw new LinkHttpError(
|
|
9967
|
-
400,
|
|
9968
|
-
"media_source_not_file",
|
|
9969
|
-
"Hermes output media source is not a file"
|
|
9970
|
-
);
|
|
9971
|
-
}
|
|
9972
|
-
if (fileStat.size > MAX_IMPORTED_BLOB_BYTES) {
|
|
9973
|
-
throw new LinkHttpError(
|
|
9974
|
-
413,
|
|
9975
|
-
"media_source_too_large",
|
|
9976
|
-
"Hermes output media source is too large"
|
|
9977
|
-
);
|
|
9978
|
-
}
|
|
9979
|
-
return deps.writeBlob(conversationId, {
|
|
9980
|
-
bytes: await readFile9(sourcePath),
|
|
9981
|
-
filename: path14.basename(sourcePath),
|
|
9982
|
-
mime: source.mime ?? inferMimeType(sourcePath)
|
|
9983
|
-
});
|
|
9984
10382
|
}
|
|
9985
|
-
function
|
|
9986
|
-
return {
|
|
9987
|
-
key: sourceKey,
|
|
9988
|
-
filename: sanitizeFilename(reference.path, "attachment"),
|
|
9989
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
9990
|
-
...isNodeError10(error) && error.code ? { code: error.code } : {}
|
|
9991
|
-
};
|
|
10383
|
+
function createConversationId() {
|
|
10384
|
+
return `conv_${randomUUID6().replaceAll("-", "")}`;
|
|
9992
10385
|
}
|
|
9993
|
-
function
|
|
9994
|
-
|
|
10386
|
+
function isoFromHermesTime(value) {
|
|
10387
|
+
const numeric = readNumber2(value);
|
|
10388
|
+
if (!numeric || numeric <= 0) {
|
|
10389
|
+
return void 0;
|
|
10390
|
+
}
|
|
10391
|
+
const millis = numeric > 1e10 ? numeric : numeric * 1e3;
|
|
10392
|
+
return new Date(millis).toISOString();
|
|
9995
10393
|
}
|
|
9996
10394
|
function readString9(payload, key) {
|
|
9997
10395
|
const value = payload[key];
|
|
9998
10396
|
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
9999
10397
|
}
|
|
10000
|
-
function
|
|
10001
|
-
return typeof value === "
|
|
10398
|
+
function readNumber2(value) {
|
|
10399
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
10400
|
+
}
|
|
10401
|
+
function readBoolean(value) {
|
|
10402
|
+
if (value === true || value === 1) {
|
|
10403
|
+
return true;
|
|
10404
|
+
}
|
|
10405
|
+
if (typeof value === "string") {
|
|
10406
|
+
return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
|
|
10407
|
+
}
|
|
10408
|
+
return false;
|
|
10002
10409
|
}
|
|
10003
10410
|
function isNodeError10(error, code) {
|
|
10004
|
-
return typeof error === "object" && error !== null && "code" in error &&
|
|
10411
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
10005
10412
|
}
|
|
10006
10413
|
|
|
10007
10414
|
// src/conversations/run-lifecycle.ts
|
|
@@ -10400,9 +10807,7 @@ function readString10(payload, key) {
|
|
|
10400
10807
|
|
|
10401
10808
|
// src/conversations/history-builder.ts
|
|
10402
10809
|
import { readFile as readFile10, stat as stat9 } from "fs/promises";
|
|
10403
|
-
import { createRequire as createRequire4 } from "module";
|
|
10404
10810
|
import path15 from "path";
|
|
10405
|
-
var nodeRequire4 = createRequire4(import.meta.url);
|
|
10406
10811
|
var HISTORY_ROLES = /* @__PURE__ */ new Set(["user", "assistant"]);
|
|
10407
10812
|
var HERMES_HISTORY_COLUMNS = [
|
|
10408
10813
|
"role",
|
|
@@ -10564,11 +10969,8 @@ async function readHermesJsonlHistory(sessionsDir, sessionId) {
|
|
|
10564
10969
|
function readHistoryRows(dbPath, sessionId) {
|
|
10565
10970
|
let db = null;
|
|
10566
10971
|
try {
|
|
10567
|
-
|
|
10568
|
-
|
|
10569
|
-
);
|
|
10570
|
-
db = new DatabaseSync(dbPath, {
|
|
10571
|
-
readOnly: true,
|
|
10972
|
+
db = openSqliteDatabase(dbPath, {
|
|
10973
|
+
readonly: true,
|
|
10572
10974
|
timeout: 1e3
|
|
10573
10975
|
});
|
|
10574
10976
|
const columns = readTableColumns2(db, "messages");
|
|
@@ -11278,8 +11680,9 @@ function readChatCompletionUsage(payload) {
|
|
|
11278
11680
|
const input = readInteger2(usage, "prompt_tokens") ?? readInteger2(usage, "input_tokens");
|
|
11279
11681
|
const output = readInteger2(usage, "completion_tokens") ?? readInteger2(usage, "output_tokens");
|
|
11280
11682
|
const total = readInteger2(usage, "total_tokens");
|
|
11281
|
-
const contextWindow = readInteger2(usage, "context_window") ?? readInteger2(usage, "context_max");
|
|
11683
|
+
const contextWindow = readInteger2(usage, "context_window") ?? readInteger2(usage, "context_max") ?? readInteger2(usage, "context_length");
|
|
11282
11684
|
const explicitContextTokens = readInteger2(usage, "context_tokens") ?? readInteger2(usage, "context_used") ?? readInteger2(usage, "current_context_tokens") ?? readInteger2(usage, "last_prompt_tokens");
|
|
11685
|
+
const explicitUsagePercent = readInteger2(usage, "usage_percent") ?? readInteger2(usage, "context_percent");
|
|
11283
11686
|
if (input === void 0 && output === void 0 && total === void 0) {
|
|
11284
11687
|
return void 0;
|
|
11285
11688
|
}
|
|
@@ -11290,7 +11693,7 @@ function readChatCompletionUsage(payload) {
|
|
|
11290
11693
|
...explicitContextTokens !== void 0 ? { context_tokens: explicitContextTokens } : {},
|
|
11291
11694
|
...contextWindow !== void 0 ? { context_window: contextWindow } : {},
|
|
11292
11695
|
...explicitContextTokens !== void 0 && contextWindow ? {
|
|
11293
|
-
usage_percent: Math.min(
|
|
11696
|
+
usage_percent: explicitUsagePercent !== void 0 ? Math.min(100, explicitUsagePercent) : Math.min(
|
|
11294
11697
|
100,
|
|
11295
11698
|
Math.round(explicitContextTokens / contextWindow * 100)
|
|
11296
11699
|
)
|
|
@@ -11437,10 +11840,20 @@ var ConversationRunLifecycle = class {
|
|
|
11437
11840
|
});
|
|
11438
11841
|
return void 0;
|
|
11439
11842
|
});
|
|
11843
|
+
const instructions = buildRunInstructions(run, deliveryStagingDir);
|
|
11844
|
+
const estimatedUsage = estimateContextUsage({
|
|
11845
|
+
conversationHistory: conversationHistory.messages,
|
|
11846
|
+
currentInput: resolvedInput,
|
|
11847
|
+
instructions,
|
|
11848
|
+
contextWindow: run.context_window
|
|
11849
|
+
});
|
|
11850
|
+
if (estimatedUsage) {
|
|
11851
|
+
await this.updateRun(conversationId, runId, { usage: estimatedUsage });
|
|
11852
|
+
}
|
|
11440
11853
|
const response = await streamHermesResponses(
|
|
11441
11854
|
{
|
|
11442
11855
|
input: resolvedInput,
|
|
11443
|
-
instructions
|
|
11856
|
+
instructions,
|
|
11444
11857
|
session_id: hermesSessionId,
|
|
11445
11858
|
model: run.model,
|
|
11446
11859
|
...previousResponseId ? { previous_response_id: previousResponseId } : {},
|
|
@@ -11943,7 +12356,7 @@ ${details.join("\n")}` : emptyHermesResponseMessage();
|
|
|
11943
12356
|
run.hermes_response_id = responseId;
|
|
11944
12357
|
}
|
|
11945
12358
|
if (usage) {
|
|
11946
|
-
run.usage = usage;
|
|
12359
|
+
run.usage = mergeRunUsage(run.usage, usage);
|
|
11947
12360
|
}
|
|
11948
12361
|
if (assistant) {
|
|
11949
12362
|
assistant.status = "completed";
|
|
@@ -11995,7 +12408,7 @@ ${details.join("\n")}` : emptyHermesResponseMessage();
|
|
|
11995
12408
|
const visibleMessage = formatFailureMessage(message, run.error_detail);
|
|
11996
12409
|
const usage = readUsage(source?.payload);
|
|
11997
12410
|
if (usage) {
|
|
11998
|
-
run.usage = usage;
|
|
12411
|
+
run.usage = mergeRunUsage(run.usage, usage);
|
|
11999
12412
|
}
|
|
12000
12413
|
const assistant = snapshot.messages.find(
|
|
12001
12414
|
(item) => item.id === run.assistant_message_id
|
|
@@ -12014,6 +12427,7 @@ ${details.join("\n")}` : emptyHermesResponseMessage();
|
|
|
12014
12427
|
}
|
|
12015
12428
|
}
|
|
12016
12429
|
await this.deps.writeSnapshot(conversationId, snapshot);
|
|
12430
|
+
const contextUsage = contextUsagePayload(run);
|
|
12017
12431
|
await this.deps.appendEvent(conversationId, {
|
|
12018
12432
|
type: "run.failed",
|
|
12019
12433
|
message_id: assistant?.id,
|
|
@@ -12022,6 +12436,7 @@ ${details.join("\n")}` : emptyHermesResponseMessage();
|
|
|
12022
12436
|
error: { message },
|
|
12023
12437
|
...run.error_detail ? { error_detail: run.error_detail } : {},
|
|
12024
12438
|
run,
|
|
12439
|
+
...contextUsage ? { context: contextUsage } : {},
|
|
12025
12440
|
...source ? { hermes: source.payload } : {}
|
|
12026
12441
|
},
|
|
12027
12442
|
...source ? { raw: { format: "hermes-run-event", payload: source.rawPayload } } : {}
|
|
@@ -12353,6 +12768,7 @@ function contextUsagePayload(run) {
|
|
|
12353
12768
|
source: "unknown"
|
|
12354
12769
|
};
|
|
12355
12770
|
}
|
|
12771
|
+
const contextSource = usage.context_source ?? "explicit";
|
|
12356
12772
|
return {
|
|
12357
12773
|
input_tokens: contextTokens,
|
|
12358
12774
|
output_tokens: usage?.output_tokens ?? 0,
|
|
@@ -12360,7 +12776,7 @@ function contextUsagePayload(run) {
|
|
|
12360
12776
|
...contextWindow ? { context_window: contextWindow } : {},
|
|
12361
12777
|
used_tokens: contextTokens,
|
|
12362
12778
|
...contextWindow ? { window_tokens: contextWindow } : {},
|
|
12363
|
-
source:
|
|
12779
|
+
source: contextSource,
|
|
12364
12780
|
...usage?.usage_percent !== void 0 ? { usage_percent: usage.usage_percent } : contextWindow ? {
|
|
12365
12781
|
usage_percent: Math.min(
|
|
12366
12782
|
100,
|
|
@@ -12369,6 +12785,41 @@ function contextUsagePayload(run) {
|
|
|
12369
12785
|
} : {}
|
|
12370
12786
|
};
|
|
12371
12787
|
}
|
|
12788
|
+
function mergeRunUsage(previous, next) {
|
|
12789
|
+
const nextContextWindow = next.context_window ?? previous?.context_window;
|
|
12790
|
+
const nextContextTokens = next.context_tokens ?? refineEstimatedContextTokens(
|
|
12791
|
+
previous,
|
|
12792
|
+
next.input_tokens,
|
|
12793
|
+
nextContextWindow
|
|
12794
|
+
);
|
|
12795
|
+
const nextContextSource = next.context_tokens !== void 0 ? next.context_source ?? "explicit" : nextContextTokens !== void 0 ? previous?.context_source : void 0;
|
|
12796
|
+
return {
|
|
12797
|
+
...next,
|
|
12798
|
+
...nextContextTokens !== void 0 ? { context_tokens: nextContextTokens } : {},
|
|
12799
|
+
...nextContextWindow !== void 0 ? { context_window: nextContextWindow } : {},
|
|
12800
|
+
...nextContextSource ? { context_source: nextContextSource } : {},
|
|
12801
|
+
...nextContextTokens !== void 0 && nextContextWindow ? {
|
|
12802
|
+
usage_percent: next.usage_percent ?? previous?.usage_percent ?? Math.min(
|
|
12803
|
+
100,
|
|
12804
|
+
Math.round(nextContextTokens / nextContextWindow * 100)
|
|
12805
|
+
)
|
|
12806
|
+
} : {}
|
|
12807
|
+
};
|
|
12808
|
+
}
|
|
12809
|
+
function refineEstimatedContextTokens(previous, inputTokens, contextWindow) {
|
|
12810
|
+
if (previous?.context_source !== "estimated") {
|
|
12811
|
+
return void 0;
|
|
12812
|
+
}
|
|
12813
|
+
const currentEstimate = previous.context_tokens;
|
|
12814
|
+
if (currentEstimate === void 0) {
|
|
12815
|
+
return void 0;
|
|
12816
|
+
}
|
|
12817
|
+
const upperBound = inputTokens > 0 ? contextWindow ? Math.min(inputTokens, contextWindow) : inputTokens : contextWindow;
|
|
12818
|
+
if (upperBound === void 0) {
|
|
12819
|
+
return currentEstimate;
|
|
12820
|
+
}
|
|
12821
|
+
return Math.min(currentEstimate, upperBound);
|
|
12822
|
+
}
|
|
12372
12823
|
function findPreviousHermesResponseId(snapshot, run) {
|
|
12373
12824
|
const currentProfile = normalizeRunProfileForCompare(run.profile);
|
|
12374
12825
|
if (!currentProfile) {
|
|
@@ -12510,6 +12961,12 @@ var ConversationService = class {
|
|
|
12510
12961
|
async listConversations() {
|
|
12511
12962
|
return this.queries.listConversations();
|
|
12512
12963
|
}
|
|
12964
|
+
listConversationPage(input = {}) {
|
|
12965
|
+
return this.queries.listConversationPage(input);
|
|
12966
|
+
}
|
|
12967
|
+
searchConversationPage(input = {}) {
|
|
12968
|
+
return this.queries.searchConversationPage(input);
|
|
12969
|
+
}
|
|
12513
12970
|
async getStatistics(filter = {}) {
|
|
12514
12971
|
return readLinkStatistics(this.paths, filter);
|
|
12515
12972
|
}
|
|
@@ -13972,13 +14429,84 @@ function isLanHost(hostname) {
|
|
|
13972
14429
|
}
|
|
13973
14430
|
|
|
13974
14431
|
// src/http/sse.ts
|
|
14432
|
+
var DEFAULT_SSE_RETRY_MS = 1e3;
|
|
14433
|
+
var DEFAULT_SSE_HEARTBEAT_MS = 15e3;
|
|
14434
|
+
function beginSseStream(request, response, options = {}) {
|
|
14435
|
+
const retryMs = normalizeRetryMs(options.retryMs);
|
|
14436
|
+
const heartbeatMs = Math.max(1e3, options.heartbeatMs ?? DEFAULT_SSE_HEARTBEAT_MS);
|
|
14437
|
+
response.statusCode = 200;
|
|
14438
|
+
response.setHeader("content-type", "text/event-stream; charset=utf-8");
|
|
14439
|
+
response.setHeader("cache-control", "no-store");
|
|
14440
|
+
response.setHeader("connection", "keep-alive");
|
|
14441
|
+
response.flushHeaders();
|
|
14442
|
+
writeSseRetry(response, retryMs);
|
|
14443
|
+
writeSseComment(response, options.initialComment ?? "connected");
|
|
14444
|
+
let closed = false;
|
|
14445
|
+
let heartbeat = null;
|
|
14446
|
+
const cleanup = () => {
|
|
14447
|
+
if (closed) {
|
|
14448
|
+
return;
|
|
14449
|
+
}
|
|
14450
|
+
closed = true;
|
|
14451
|
+
if (heartbeat != null) {
|
|
14452
|
+
clearInterval(heartbeat);
|
|
14453
|
+
heartbeat = null;
|
|
14454
|
+
}
|
|
14455
|
+
request.off("close", cleanup);
|
|
14456
|
+
response.off("close", cleanup);
|
|
14457
|
+
options.onClose?.();
|
|
14458
|
+
if (!response.writableEnded && !response.destroyed) {
|
|
14459
|
+
response.end();
|
|
14460
|
+
}
|
|
14461
|
+
};
|
|
14462
|
+
heartbeat = setInterval(() => {
|
|
14463
|
+
if (response.writableEnded || response.destroyed) {
|
|
14464
|
+
cleanup();
|
|
14465
|
+
return;
|
|
14466
|
+
}
|
|
14467
|
+
writeSseComment(response);
|
|
14468
|
+
}, heartbeatMs);
|
|
14469
|
+
heartbeat.unref();
|
|
14470
|
+
request.once("close", cleanup);
|
|
14471
|
+
response.once("close", cleanup);
|
|
14472
|
+
return cleanup;
|
|
14473
|
+
}
|
|
13975
14474
|
function writeSseEvent(response, event) {
|
|
13976
|
-
response
|
|
14475
|
+
writeJsonSseEvent(response, {
|
|
14476
|
+
event: event.type,
|
|
14477
|
+
data: event,
|
|
14478
|
+
id: event.seq
|
|
14479
|
+
});
|
|
14480
|
+
}
|
|
14481
|
+
function writeJsonSseEvent(response, event) {
|
|
14482
|
+
if (event.retryMs != null) {
|
|
14483
|
+
response.write(`retry: ${normalizeRetryMs(event.retryMs)}
|
|
14484
|
+
`);
|
|
14485
|
+
}
|
|
14486
|
+
if (event.id != null && event.id !== "") {
|
|
14487
|
+
response.write(`id: ${event.id}
|
|
14488
|
+
`);
|
|
14489
|
+
}
|
|
14490
|
+
response.write(`event: ${event.event}
|
|
14491
|
+
`);
|
|
14492
|
+
response.write(`data: ${JSON.stringify(event.data)}
|
|
14493
|
+
|
|
13977
14494
|
`);
|
|
13978
|
-
|
|
14495
|
+
}
|
|
14496
|
+
function writeSseComment(response, comment = "keep-alive") {
|
|
14497
|
+
response.write(`: ${comment}
|
|
14498
|
+
|
|
14499
|
+
`);
|
|
14500
|
+
}
|
|
14501
|
+
function writeSseRetry(response, retryMs) {
|
|
14502
|
+
response.write(`retry: ${normalizeRetryMs(retryMs)}
|
|
13979
14503
|
|
|
13980
14504
|
`);
|
|
13981
14505
|
}
|
|
14506
|
+
function normalizeRetryMs(retryMs) {
|
|
14507
|
+
const parsed = Number.isFinite(retryMs) ? Math.trunc(retryMs) : DEFAULT_SSE_RETRY_MS;
|
|
14508
|
+
return parsed >= 0 ? parsed : DEFAULT_SSE_RETRY_MS;
|
|
14509
|
+
}
|
|
13982
14510
|
|
|
13983
14511
|
// src/http/routes/conversations.ts
|
|
13984
14512
|
function registerConversationRoutes(router, options) {
|
|
@@ -13986,9 +14514,28 @@ function registerConversationRoutes(router, options) {
|
|
|
13986
14514
|
router.get("/api/v1/conversations", async (ctx) => {
|
|
13987
14515
|
await authenticateRequest(ctx, paths);
|
|
13988
14516
|
ctx.set("cache-control", "no-store");
|
|
14517
|
+
const result = await conversations.listConversationPage({
|
|
14518
|
+
limit: readLimit(ctx.query.limit),
|
|
14519
|
+
cursor: readQueryString(ctx.query.cursor) ?? readQueryString(ctx.query.after) ?? readQueryString(ctx.query.page_cursor)
|
|
14520
|
+
});
|
|
14521
|
+
ctx.body = {
|
|
14522
|
+
ok: true,
|
|
14523
|
+
conversations: result.conversations,
|
|
14524
|
+
page: result.page
|
|
14525
|
+
};
|
|
14526
|
+
});
|
|
14527
|
+
router.get("/api/v1/conversations/search", async (ctx) => {
|
|
14528
|
+
await authenticateRequest(ctx, paths);
|
|
14529
|
+
ctx.set("cache-control", "no-store");
|
|
14530
|
+
const result = await conversations.searchConversationPage({
|
|
14531
|
+
limit: readLimit(ctx.query.limit),
|
|
14532
|
+
cursor: readQueryString(ctx.query.cursor) ?? readQueryString(ctx.query.after) ?? readQueryString(ctx.query.page_cursor),
|
|
14533
|
+
query: readQueryString(ctx.query.query) ?? readQueryString(ctx.query.q) ?? readQueryString(ctx.query.keyword) ?? ""
|
|
14534
|
+
});
|
|
13989
14535
|
ctx.body = {
|
|
13990
14536
|
ok: true,
|
|
13991
|
-
conversations:
|
|
14537
|
+
conversations: result.conversations,
|
|
14538
|
+
page: result.page
|
|
13992
14539
|
};
|
|
13993
14540
|
});
|
|
13994
14541
|
router.post("/api/v1/conversations", async (ctx) => {
|
|
@@ -14022,49 +14569,44 @@ function registerConversationRoutes(router, options) {
|
|
|
14022
14569
|
const notificationOnly = mode === "notifications";
|
|
14023
14570
|
ctx.respond = false;
|
|
14024
14571
|
const response = ctx.res;
|
|
14025
|
-
|
|
14026
|
-
|
|
14027
|
-
|
|
14028
|
-
|
|
14029
|
-
|
|
14572
|
+
let unsubscribe = () => {
|
|
14573
|
+
};
|
|
14574
|
+
beginSseStream(ctx.req, response, {
|
|
14575
|
+
onClose: () => unsubscribe()
|
|
14576
|
+
});
|
|
14577
|
+
unsubscribe = conversations.subscribeAll((event) => {
|
|
14030
14578
|
if (notificationOnly && !isConversationNotificationEvent(event)) {
|
|
14031
14579
|
return;
|
|
14032
14580
|
}
|
|
14033
14581
|
writeSseEvent(response, event);
|
|
14034
14582
|
});
|
|
14035
|
-
const cleanup = () => {
|
|
14036
|
-
unsubscribe();
|
|
14037
|
-
response.end();
|
|
14038
|
-
};
|
|
14039
|
-
ctx.req.on("close", cleanup);
|
|
14040
14583
|
});
|
|
14041
14584
|
router.get("/api/v1/conversations/:conversationId/events", async (ctx) => {
|
|
14042
14585
|
await authenticateRequest(ctx, paths);
|
|
14043
|
-
const after =
|
|
14586
|
+
const after = resolveConversationEventCursor({
|
|
14587
|
+
queryAfter: ctx.query.after,
|
|
14588
|
+
lastEventIdHeader: ctx.req.headers["last-event-id"]
|
|
14589
|
+
});
|
|
14044
14590
|
const history = await conversations.listEvents(
|
|
14045
14591
|
ctx.params.conversationId,
|
|
14046
14592
|
after
|
|
14047
14593
|
);
|
|
14048
14594
|
ctx.respond = false;
|
|
14049
14595
|
const response = ctx.res;
|
|
14050
|
-
|
|
14051
|
-
|
|
14052
|
-
|
|
14053
|
-
|
|
14596
|
+
let unsubscribe = () => {
|
|
14597
|
+
};
|
|
14598
|
+
beginSseStream(ctx.req, response, {
|
|
14599
|
+
onClose: () => unsubscribe()
|
|
14600
|
+
});
|
|
14054
14601
|
for (const event of history) {
|
|
14055
14602
|
writeSseEvent(response, event);
|
|
14056
14603
|
}
|
|
14057
|
-
|
|
14604
|
+
unsubscribe = conversations.subscribe(
|
|
14058
14605
|
ctx.params.conversationId,
|
|
14059
14606
|
(event) => {
|
|
14060
14607
|
writeSseEvent(response, event);
|
|
14061
14608
|
}
|
|
14062
14609
|
);
|
|
14063
|
-
const cleanup = () => {
|
|
14064
|
-
unsubscribe();
|
|
14065
|
-
response.end();
|
|
14066
|
-
};
|
|
14067
|
-
ctx.req.on("close", cleanup);
|
|
14068
14610
|
});
|
|
14069
14611
|
router.post("/api/v1/conversations/:conversationId/messages", async (ctx) => {
|
|
14070
14612
|
await authenticateRequest(ctx, paths);
|
|
@@ -14267,6 +14809,19 @@ function registerConversationRoutes(router, options) {
|
|
|
14267
14809
|
}
|
|
14268
14810
|
);
|
|
14269
14811
|
}
|
|
14812
|
+
function resolveConversationEventCursor(input) {
|
|
14813
|
+
const queryAfter = readInteger3(input.queryAfter) ?? 0;
|
|
14814
|
+
const headerAfter = readNonNegativeIntegerHeader(input.lastEventIdHeader) ?? 0;
|
|
14815
|
+
return Math.max(queryAfter, headerAfter);
|
|
14816
|
+
}
|
|
14817
|
+
function readNonNegativeIntegerHeader(value) {
|
|
14818
|
+
const raw = Array.isArray(value) ? value[0] : value;
|
|
14819
|
+
if (!raw) {
|
|
14820
|
+
return null;
|
|
14821
|
+
}
|
|
14822
|
+
const parsed = Number.parseInt(raw, 10);
|
|
14823
|
+
return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : null;
|
|
14824
|
+
}
|
|
14270
14825
|
function contentDispositionInline(filename) {
|
|
14271
14826
|
const fallback = asciiFilenameFallback(filename);
|
|
14272
14827
|
return `inline; filename="${fallback}"; filename*=UTF-8''${encodeRfc5987Value(filename)}`;
|
|
@@ -18260,22 +18815,18 @@ function registerProfileRoutes(router, options) {
|
|
|
18260
18815
|
await authenticateRequest(ctx, paths);
|
|
18261
18816
|
ctx.respond = false;
|
|
18262
18817
|
const response = ctx.res;
|
|
18263
|
-
|
|
18264
|
-
|
|
18265
|
-
|
|
18266
|
-
|
|
18818
|
+
let unsubscribe = () => {
|
|
18819
|
+
};
|
|
18820
|
+
beginSseStream(ctx.req, response, {
|
|
18821
|
+
onClose: () => unsubscribe()
|
|
18822
|
+
});
|
|
18267
18823
|
writeProfileCreationSseEvent(
|
|
18268
18824
|
response,
|
|
18269
18825
|
await readHermesProfileCreationStatus(paths)
|
|
18270
18826
|
);
|
|
18271
|
-
|
|
18827
|
+
unsubscribe = subscribeHermesProfileCreationStatus((status) => {
|
|
18272
18828
|
writeProfileCreationSseEvent(response, status);
|
|
18273
18829
|
});
|
|
18274
|
-
const cleanup = () => {
|
|
18275
|
-
unsubscribe();
|
|
18276
|
-
response.end();
|
|
18277
|
-
};
|
|
18278
|
-
ctx.req.on("close", cleanup);
|
|
18279
18830
|
});
|
|
18280
18831
|
router.get("/api/v1/profiles/:name/status", async (ctx) => {
|
|
18281
18832
|
await authenticateRequest(ctx, paths);
|
|
@@ -18491,10 +19042,10 @@ function isProfileAvatarUrl(value) {
|
|
|
18491
19042
|
return isHttpUrl(value) || /^data:image\/[a-z0-9.+-]+;base64,/iu.test(value);
|
|
18492
19043
|
}
|
|
18493
19044
|
function writeProfileCreationSseEvent(response, status) {
|
|
18494
|
-
response
|
|
18495
|
-
|
|
18496
|
-
|
|
18497
|
-
|
|
19045
|
+
writeJsonSseEvent(response, {
|
|
19046
|
+
event: "profile.creation.status",
|
|
19047
|
+
data: status
|
|
19048
|
+
});
|
|
18498
19049
|
}
|
|
18499
19050
|
|
|
18500
19051
|
// src/http/routes/runs.ts
|
|
@@ -19784,6 +20335,7 @@ var DEFAULT_STARTUP_REPORT_MIN_INTERVAL_MS = 15 * 6e4;
|
|
|
19784
20335
|
function startLanIpMonitor(options) {
|
|
19785
20336
|
let running = false;
|
|
19786
20337
|
let closed = false;
|
|
20338
|
+
let current = Promise.resolve();
|
|
19787
20339
|
const check = async (context = {}) => {
|
|
19788
20340
|
if (running || closed) {
|
|
19789
20341
|
return;
|
|
@@ -19799,15 +20351,16 @@ function startLanIpMonitor(options) {
|
|
|
19799
20351
|
running = false;
|
|
19800
20352
|
}
|
|
19801
20353
|
};
|
|
19802
|
-
|
|
20354
|
+
current = check({ forceReport: true, publishToRelay: true });
|
|
19803
20355
|
const timer = setInterval(() => {
|
|
19804
|
-
|
|
20356
|
+
current = check();
|
|
19805
20357
|
}, options.intervalMs ?? DEFAULT_INTERVAL_MS);
|
|
19806
20358
|
timer.unref?.();
|
|
19807
20359
|
return {
|
|
19808
|
-
close() {
|
|
20360
|
+
async close() {
|
|
19809
20361
|
closed = true;
|
|
19810
20362
|
clearInterval(timer);
|
|
20363
|
+
await current.catch(() => void 0);
|
|
19811
20364
|
}
|
|
19812
20365
|
};
|
|
19813
20366
|
}
|
|
@@ -19877,6 +20430,7 @@ async function checkLanIpChange(options, context = {}) {
|
|
|
19877
20430
|
// src/daemon/scheduler.ts
|
|
19878
20431
|
function startCronDeliveryScheduler(options) {
|
|
19879
20432
|
let running = false;
|
|
20433
|
+
let current = Promise.resolve();
|
|
19880
20434
|
const syncCronDeliveries = async () => {
|
|
19881
20435
|
if (running) {
|
|
19882
20436
|
return;
|
|
@@ -19897,17 +20451,19 @@ function startCronDeliveryScheduler(options) {
|
|
|
19897
20451
|
}
|
|
19898
20452
|
};
|
|
19899
20453
|
const timer = setInterval(() => {
|
|
19900
|
-
|
|
20454
|
+
current = syncCronDeliveries();
|
|
19901
20455
|
}, options.intervalMs ?? 3e4);
|
|
19902
20456
|
timer.unref?.();
|
|
19903
20457
|
return {
|
|
19904
|
-
close() {
|
|
20458
|
+
async close() {
|
|
19905
20459
|
clearInterval(timer);
|
|
20460
|
+
await current.catch(() => void 0);
|
|
19906
20461
|
}
|
|
19907
20462
|
};
|
|
19908
20463
|
}
|
|
19909
20464
|
function startHermesSessionSyncScheduler(options) {
|
|
19910
20465
|
let running = false;
|
|
20466
|
+
let current = Promise.resolve();
|
|
19911
20467
|
const syncSessions = async () => {
|
|
19912
20468
|
if (running) {
|
|
19913
20469
|
return;
|
|
@@ -19924,12 +20480,13 @@ function startHermesSessionSyncScheduler(options) {
|
|
|
19924
20480
|
}
|
|
19925
20481
|
};
|
|
19926
20482
|
const timer = setInterval(() => {
|
|
19927
|
-
|
|
20483
|
+
current = syncSessions();
|
|
19928
20484
|
}, options.intervalMs ?? 10 * 60 * 1e3);
|
|
19929
20485
|
timer.unref?.();
|
|
19930
20486
|
return {
|
|
19931
|
-
close() {
|
|
20487
|
+
async close() {
|
|
19932
20488
|
clearInterval(timer);
|
|
20489
|
+
await current.catch(() => void 0);
|
|
19933
20490
|
}
|
|
19934
20491
|
};
|
|
19935
20492
|
}
|
|
@@ -19953,8 +20510,9 @@ async function startLinkService(options = {}) {
|
|
|
19953
20510
|
}
|
|
19954
20511
|
const conversations = new ConversationService(paths, logger);
|
|
19955
20512
|
await conversations.rebuildStatisticsIndex();
|
|
20513
|
+
let hermesSessionSync = Promise.resolve();
|
|
19956
20514
|
const triggerHermesSessionSync = () => {
|
|
19957
|
-
|
|
20515
|
+
hermesSessionSync = Promise.resolve().then(() => conversations.syncHermesSessions()).then(() => void 0).catch((error) => {
|
|
19958
20516
|
void logger.warn("hermes_session_sync_failed", {
|
|
19959
20517
|
error: error instanceof Error ? error.message : String(error)
|
|
19960
20518
|
});
|
|
@@ -20028,11 +20586,14 @@ async function startLinkService(options = {}) {
|
|
|
20028
20586
|
}
|
|
20029
20587
|
return {
|
|
20030
20588
|
async close() {
|
|
20031
|
-
scheduler.close();
|
|
20032
|
-
hermesSessionSyncScheduler.close();
|
|
20033
|
-
lanIpMonitor.close();
|
|
20034
20589
|
relay?.close();
|
|
20035
20590
|
await closeServer(server);
|
|
20591
|
+
await Promise.all([
|
|
20592
|
+
scheduler.close(),
|
|
20593
|
+
hermesSessionSyncScheduler.close(),
|
|
20594
|
+
lanIpMonitor.close(),
|
|
20595
|
+
hermesSessionSync.catch(() => void 0)
|
|
20596
|
+
]);
|
|
20036
20597
|
await logger.info("service_stopped");
|
|
20037
20598
|
await logger.flush();
|
|
20038
20599
|
if (options.writePidFile) {
|
|
@@ -21646,26 +22207,22 @@ function registerHermesUpdateRoutes(router, options) {
|
|
|
21646
22207
|
await authenticateRequest(ctx, paths);
|
|
21647
22208
|
ctx.respond = false;
|
|
21648
22209
|
const response = ctx.res;
|
|
21649
|
-
|
|
21650
|
-
|
|
21651
|
-
|
|
21652
|
-
|
|
22210
|
+
let unsubscribe = () => {
|
|
22211
|
+
};
|
|
22212
|
+
beginSseStream(ctx.req, response, {
|
|
22213
|
+
onClose: () => unsubscribe()
|
|
22214
|
+
});
|
|
21653
22215
|
writeUpdateSseEvent(response, await readHermesUpdateStatus(paths));
|
|
21654
|
-
|
|
22216
|
+
unsubscribe = subscribeHermesUpdateStatus((status) => {
|
|
21655
22217
|
writeUpdateSseEvent(response, status);
|
|
21656
22218
|
});
|
|
21657
|
-
const cleanup = () => {
|
|
21658
|
-
unsubscribe();
|
|
21659
|
-
response.end();
|
|
21660
|
-
};
|
|
21661
|
-
ctx.req.on("close", cleanup);
|
|
21662
22219
|
});
|
|
21663
22220
|
}
|
|
21664
22221
|
function writeUpdateSseEvent(response, status) {
|
|
21665
|
-
response
|
|
21666
|
-
|
|
21667
|
-
|
|
21668
|
-
|
|
22222
|
+
writeJsonSseEvent(response, {
|
|
22223
|
+
event: "hermes.update.status",
|
|
22224
|
+
data: status
|
|
22225
|
+
});
|
|
21669
22226
|
}
|
|
21670
22227
|
|
|
21671
22228
|
// src/http/routes/link-updates.ts
|
|
@@ -21695,26 +22252,22 @@ function registerLinkUpdateRoutes(router, options) {
|
|
|
21695
22252
|
await authenticateRequest(ctx, paths);
|
|
21696
22253
|
ctx.respond = false;
|
|
21697
22254
|
const response = ctx.res;
|
|
21698
|
-
|
|
21699
|
-
|
|
21700
|
-
|
|
21701
|
-
|
|
22255
|
+
let unsubscribe = () => {
|
|
22256
|
+
};
|
|
22257
|
+
beginSseStream(ctx.req, response, {
|
|
22258
|
+
onClose: () => unsubscribe()
|
|
22259
|
+
});
|
|
21702
22260
|
writeUpdateSseEvent2(response, await readLinkUpdateStatus(paths));
|
|
21703
|
-
|
|
22261
|
+
unsubscribe = subscribeLinkUpdateStatus((status) => {
|
|
21704
22262
|
writeUpdateSseEvent2(response, status);
|
|
21705
22263
|
});
|
|
21706
|
-
const cleanup = () => {
|
|
21707
|
-
unsubscribe();
|
|
21708
|
-
response.end();
|
|
21709
|
-
};
|
|
21710
|
-
ctx.req.on("close", cleanup);
|
|
21711
22264
|
});
|
|
21712
22265
|
}
|
|
21713
22266
|
function writeUpdateSseEvent2(response, status) {
|
|
21714
|
-
response
|
|
21715
|
-
|
|
21716
|
-
|
|
21717
|
-
|
|
22267
|
+
writeJsonSseEvent(response, {
|
|
22268
|
+
event: "link.update.status",
|
|
22269
|
+
data: status
|
|
22270
|
+
});
|
|
21718
22271
|
}
|
|
21719
22272
|
|
|
21720
22273
|
// src/http/routes/pairing.ts
|