clawconnect-agent 0.1.41 → 0.2.0
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/.env.example +3 -0
- package/README.md +2 -1
- package/README.zh-CN.md +2 -1
- package/dist/commands/status.js +7 -2
- package/dist/commands/status.js.map +1 -1
- package/dist/config/env.d.ts +1 -1
- package/dist/config/env.js +3 -0
- package/dist/config/env.js.map +1 -1
- package/dist/hermes/hermes-relay-manager.d.ts +2 -3
- package/dist/hermes/hermes-relay-manager.js +19 -32
- package/dist/hermes/hermes-relay-manager.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-api-client.js +28 -13
- package/dist/hermes/runtime/hermes-runtime-api-client.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-api-settings.d.ts +8 -3
- package/dist/hermes/runtime/hermes-runtime-api-settings.js +120 -9
- package/dist/hermes/runtime/hermes-runtime-api-settings.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-chat.js +51 -0
- package/dist/hermes/runtime/hermes-runtime-chat.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-state-db.d.ts +8 -0
- package/dist/hermes/runtime/hermes-runtime-state-db.js +124 -4
- package/dist/hermes/runtime/hermes-runtime-state-db.js.map +1 -1
- package/package.json +1 -1
- package/dist/hermes/relay/hermes-state-db-realtime.d.ts +0 -99
- package/dist/hermes/relay/hermes-state-db-realtime.js +0 -833
- package/dist/hermes/relay/hermes-state-db-realtime.js.map +0 -1
|
@@ -1,833 +0,0 @@
|
|
|
1
|
-
import { execFile as execFileCb, spawn } from "node:child_process";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
import { promisify } from "node:util";
|
|
5
|
-
import { canonicalizeMobileAssistantText } from "../../core/relay/mobile-chat-run-bridge.js";
|
|
6
|
-
import { parseCanonicalTimelineEvent, } from "../../core/relay/timeline-event-log.js";
|
|
7
|
-
import { SUBPROCESS_ENV, } from "../runtime/hermes-runtime-process.js";
|
|
8
|
-
import { resolveHermesPythonBin, resolveHermesStateDbPath } from "../runtime/hermes-runtime-paths.js";
|
|
9
|
-
import { hermesStateDbMessageId } from "../runtime/hermes-state-db-message-identity.js";
|
|
10
|
-
const execFile = promisify(execFileCb);
|
|
11
|
-
const CLAWCONNECT_MOBILE_TURN_MARKER = "[ClawConnect mobile turn]";
|
|
12
|
-
const CLAWCONNECT_MOBILE_BRIDGE_MARKER = "[ClawConnect mobile bridge]";
|
|
13
|
-
const HERMES_STATE_DB_REALTIME_QUERY_TIMEOUT_MS = 5_000;
|
|
14
|
-
const HERMES_STATE_DB_REALTIME_QUERY_ATTEMPTS = 2;
|
|
15
|
-
const HERMES_STATE_DB_REALTIME_DEFAULT_LIMIT = 200;
|
|
16
|
-
const HERMES_STATE_DB_REALTIME_DEFAULT_POLL_INTERVAL_MS = 750;
|
|
17
|
-
const HERMES_RUNTIME_CONTEXT_HINT_REGEX = /(^|\r?\n)[ \t]*\[Hermes runtime context\][\s\S]*?(?=\r?\n[ \t]*\[ClawConnect mobile bridge\]|\r?\n[ \t]*\[ClawConnect mobile turn\]|$)/gi;
|
|
18
|
-
const HERMES_STATE_DB_REALTIME_QUERY_SCRIPT = String.raw `
|
|
19
|
-
import json
|
|
20
|
-
import sqlite3
|
|
21
|
-
import sys
|
|
22
|
-
|
|
23
|
-
db_path = sys.argv[2]
|
|
24
|
-
|
|
25
|
-
def connect():
|
|
26
|
-
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True, timeout=1.0)
|
|
27
|
-
conn.row_factory = sqlite3.Row
|
|
28
|
-
conn.execute("PRAGMA query_only=ON")
|
|
29
|
-
return conn
|
|
30
|
-
|
|
31
|
-
def message_payload(row):
|
|
32
|
-
return {
|
|
33
|
-
"id": int(row["id"]),
|
|
34
|
-
"sessionId": row["session_id"],
|
|
35
|
-
"sessionSource": row["session_source"],
|
|
36
|
-
"role": row["role"],
|
|
37
|
-
"content": row["content"],
|
|
38
|
-
"timestamp": row["timestamp"],
|
|
39
|
-
"active": bool(row["active"]),
|
|
40
|
-
"observed": bool(row["observed"]),
|
|
41
|
-
"toolName": row["tool_name"],
|
|
42
|
-
"toolCallId": row["tool_call_id"],
|
|
43
|
-
"finishReason": row["finish_reason"],
|
|
44
|
-
"toolCalls": row["tool_calls"],
|
|
45
|
-
"platformMessageId": row["platform_message_id"],
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
def query(conn, mode, args):
|
|
49
|
-
if mode == "max":
|
|
50
|
-
row = conn.execute("SELECT COALESCE(MAX(id), 0) AS max_id FROM messages").fetchone()
|
|
51
|
-
return int(row["max_id"] or 0)
|
|
52
|
-
elif mode == "rows":
|
|
53
|
-
after_id = int(args[0])
|
|
54
|
-
limit = int(args[1])
|
|
55
|
-
rows = conn.execute("""
|
|
56
|
-
SELECT
|
|
57
|
-
m.id,
|
|
58
|
-
m.session_id,
|
|
59
|
-
COALESCE(s.source, '') AS session_source,
|
|
60
|
-
m.role,
|
|
61
|
-
COALESCE(m.content, '') AS content,
|
|
62
|
-
m.timestamp,
|
|
63
|
-
COALESCE(m.active, 1) AS active,
|
|
64
|
-
COALESCE(m.observed, 0) AS observed,
|
|
65
|
-
m.tool_name,
|
|
66
|
-
m.tool_call_id,
|
|
67
|
-
m.finish_reason,
|
|
68
|
-
m.tool_calls,
|
|
69
|
-
m.platform_message_id
|
|
70
|
-
FROM messages m
|
|
71
|
-
LEFT JOIN sessions s ON s.id = m.session_id
|
|
72
|
-
WHERE m.id > ?
|
|
73
|
-
ORDER BY m.id ASC
|
|
74
|
-
LIMIT ?
|
|
75
|
-
""", (after_id, limit)).fetchall()
|
|
76
|
-
return [message_payload(row) for row in rows]
|
|
77
|
-
elif mode == "open_turn_users":
|
|
78
|
-
up_to_id = int(args[0])
|
|
79
|
-
rows = conn.execute("""
|
|
80
|
-
WITH latest_user AS (
|
|
81
|
-
SELECT session_id, MAX(id) AS message_id
|
|
82
|
-
FROM messages
|
|
83
|
-
WHERE id <= ?
|
|
84
|
-
AND role = 'user'
|
|
85
|
-
AND COALESCE(active, 1) = 1
|
|
86
|
-
GROUP BY session_id
|
|
87
|
-
),
|
|
88
|
-
latest_terminal_assistant AS (
|
|
89
|
-
SELECT m.session_id, MAX(m.id) AS message_id
|
|
90
|
-
FROM messages m
|
|
91
|
-
INNER JOIN latest_user latest
|
|
92
|
-
ON latest.session_id = m.session_id
|
|
93
|
-
WHERE m.id > latest.message_id
|
|
94
|
-
AND m.id <= ?
|
|
95
|
-
AND m.role = 'assistant'
|
|
96
|
-
AND COALESCE(m.active, 1) = 1
|
|
97
|
-
AND LOWER(COALESCE(m.finish_reason, '')) NOT IN ('tool_calls', 'function_call')
|
|
98
|
-
AND (
|
|
99
|
-
m.tool_calls IS NULL
|
|
100
|
-
OR TRIM(m.tool_calls) IN ('', '[]', '{}', 'null')
|
|
101
|
-
)
|
|
102
|
-
GROUP BY m.session_id
|
|
103
|
-
)
|
|
104
|
-
SELECT
|
|
105
|
-
m.id,
|
|
106
|
-
m.session_id,
|
|
107
|
-
COALESCE(s.source, '') AS session_source,
|
|
108
|
-
m.role,
|
|
109
|
-
COALESCE(m.content, '') AS content,
|
|
110
|
-
m.timestamp,
|
|
111
|
-
COALESCE(m.active, 1) AS active,
|
|
112
|
-
COALESCE(m.observed, 0) AS observed,
|
|
113
|
-
m.tool_name,
|
|
114
|
-
m.tool_call_id,
|
|
115
|
-
m.finish_reason,
|
|
116
|
-
m.tool_calls,
|
|
117
|
-
m.platform_message_id
|
|
118
|
-
FROM messages m
|
|
119
|
-
INNER JOIN latest_user latest
|
|
120
|
-
ON latest.session_id = m.session_id
|
|
121
|
-
LEFT JOIN latest_terminal_assistant terminal
|
|
122
|
-
ON terminal.session_id = m.session_id
|
|
123
|
-
LEFT JOIN sessions s ON s.id = m.session_id
|
|
124
|
-
WHERE (
|
|
125
|
-
m.id = latest.message_id
|
|
126
|
-
OR m.id = terminal.message_id
|
|
127
|
-
)
|
|
128
|
-
AND COALESCE(m.active, 1) = 1
|
|
129
|
-
ORDER BY m.id ASC
|
|
130
|
-
""", (up_to_id, up_to_id)).fetchall()
|
|
131
|
-
return [message_payload(row) for row in rows]
|
|
132
|
-
raise ValueError("unsupported mode")
|
|
133
|
-
|
|
134
|
-
def execute(conn, mode, args):
|
|
135
|
-
try:
|
|
136
|
-
return {"ok": True, "payload": query(conn, mode, args)}
|
|
137
|
-
except Exception as exc:
|
|
138
|
-
return {"ok": False, "error": str(exc)}
|
|
139
|
-
|
|
140
|
-
conn = connect()
|
|
141
|
-
mode = sys.argv[1]
|
|
142
|
-
if mode == "serve":
|
|
143
|
-
for line in sys.stdin:
|
|
144
|
-
request = None
|
|
145
|
-
try:
|
|
146
|
-
request = json.loads(line)
|
|
147
|
-
response = execute(conn, request.get("mode"), request.get("args") or [])
|
|
148
|
-
except Exception as exc:
|
|
149
|
-
response = {"ok": False, "error": str(exc)}
|
|
150
|
-
response["id"] = request.get("id") if isinstance(request, dict) else None
|
|
151
|
-
print(json.dumps(response, ensure_ascii=False), flush=True)
|
|
152
|
-
else:
|
|
153
|
-
print(json.dumps(execute(conn, mode, sys.argv[3:]), ensure_ascii=False))
|
|
154
|
-
`;
|
|
155
|
-
export function buildHermesStateDbRealtimePayloads(params) {
|
|
156
|
-
const cursor = cloneCursor(params.cursor);
|
|
157
|
-
const payloads = [];
|
|
158
|
-
const rows = [...params.rows].sort((left, right) => left.id - right.id);
|
|
159
|
-
for (const row of rows) {
|
|
160
|
-
if (row.id <= cursor.lastMessageId) {
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
cursor.lastMessageId = Math.max(cursor.lastMessageId, row.id);
|
|
164
|
-
if (!row.active) {
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
if (row.role === "user") {
|
|
168
|
-
const visibleText = normalizeHermesStateDbUserText(row.content);
|
|
169
|
-
if (!visibleText) {
|
|
170
|
-
delete cursor.openTurnsBySession[row.sessionId];
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
// 移动端 user 已由 Relay 接收,不重复回写;但最终 assistant 必须从
|
|
174
|
-
// state.db 用相同 runId 回补,防止 live 断线或错误的中间完成让刷新丢回答。
|
|
175
|
-
const mobileTurn = isClawConnectMobileTurn(row.content);
|
|
176
|
-
const turn = buildOpenTurn(row, mobileTurn);
|
|
177
|
-
cursor.openTurnsBySession[row.sessionId] = turn;
|
|
178
|
-
if (mobileTurn) {
|
|
179
|
-
continue;
|
|
180
|
-
}
|
|
181
|
-
payloads.push(buildUserPayload(params.gatewayId, row, turn, visibleText));
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
const turn = cursor.openTurnsBySession[row.sessionId] ?? buildOpenTurn(row, false);
|
|
185
|
-
if (turn.mobileTurn && !isTerminalHermesStateDbAssistant(row)) {
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
|
-
const visibleText = normalizeHermesStateDbOutputText(row.content);
|
|
189
|
-
if (!visibleText) {
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
payloads.push(buildOutputPayload(params.gatewayId, row, turn, visibleText));
|
|
193
|
-
}
|
|
194
|
-
return { cursor, payloads };
|
|
195
|
-
}
|
|
196
|
-
export async function queryHermesStateDbMaxMessageId(params) {
|
|
197
|
-
if (!existsSync(params.dbPath)) {
|
|
198
|
-
return 0;
|
|
199
|
-
}
|
|
200
|
-
const payload = await runHermesStateDbRealtimeQuery("max", params.dbPath, [], params.pythonBin);
|
|
201
|
-
return typeof payload === "number" && Number.isFinite(payload) ? Math.max(0, Math.floor(payload)) : 0;
|
|
202
|
-
}
|
|
203
|
-
export async function queryHermesStateDbRealtimeRows(params) {
|
|
204
|
-
if (!existsSync(params.dbPath)) {
|
|
205
|
-
return [];
|
|
206
|
-
}
|
|
207
|
-
const afterMessageId = Math.max(0, Math.floor(params.afterMessageId));
|
|
208
|
-
const limit = Math.max(1, Math.floor(params.limit ?? HERMES_STATE_DB_REALTIME_DEFAULT_LIMIT));
|
|
209
|
-
const payload = await runHermesStateDbRealtimeQuery("rows", params.dbPath, [String(afterMessageId), String(limit)], params.pythonBin);
|
|
210
|
-
return Array.isArray(payload) ? payload.flatMap(parseHermesStateDbRealtimeMessageRow) : [];
|
|
211
|
-
}
|
|
212
|
-
export async function queryHermesStateDbRealtimeOpenTurnRows(params) {
|
|
213
|
-
if (!existsSync(params.dbPath)) {
|
|
214
|
-
return [];
|
|
215
|
-
}
|
|
216
|
-
const upToMessageId = Math.max(0, Math.floor(params.upToMessageId));
|
|
217
|
-
const payload = await runHermesStateDbRealtimeQuery("open_turn_users", params.dbPath, [String(upToMessageId)], params.pythonBin);
|
|
218
|
-
return Array.isArray(payload) ? payload.flatMap(parseHermesStateDbRealtimeMessageRow) : [];
|
|
219
|
-
}
|
|
220
|
-
export function createHermesStateDbRealtimeQueryClient(params) {
|
|
221
|
-
let child;
|
|
222
|
-
let stdoutBuffer = "";
|
|
223
|
-
let stderrTail = "";
|
|
224
|
-
let nextRequestId = 1;
|
|
225
|
-
let closed = false;
|
|
226
|
-
const pending = new Map();
|
|
227
|
-
const timeoutMs = Math.max(250, Math.floor(params.timeoutMs ?? HERMES_STATE_DB_REALTIME_QUERY_TIMEOUT_MS));
|
|
228
|
-
const rejectPending = (error) => {
|
|
229
|
-
for (const request of pending.values()) {
|
|
230
|
-
clearTimeout(request.timeout);
|
|
231
|
-
request.reject(error);
|
|
232
|
-
}
|
|
233
|
-
pending.clear();
|
|
234
|
-
};
|
|
235
|
-
const stopChild = (expectedChild, error) => {
|
|
236
|
-
if (child === expectedChild) {
|
|
237
|
-
child = undefined;
|
|
238
|
-
}
|
|
239
|
-
if (error) {
|
|
240
|
-
rejectPending(error);
|
|
241
|
-
}
|
|
242
|
-
if (!expectedChild.killed) {
|
|
243
|
-
expectedChild.kill();
|
|
244
|
-
}
|
|
245
|
-
};
|
|
246
|
-
const ensureChild = () => {
|
|
247
|
-
if (closed) {
|
|
248
|
-
throw new Error("Hermes state.db realtime query client is closed");
|
|
249
|
-
}
|
|
250
|
-
if (child && !child.killed) {
|
|
251
|
-
return child;
|
|
252
|
-
}
|
|
253
|
-
stdoutBuffer = "";
|
|
254
|
-
stderrTail = "";
|
|
255
|
-
const started = spawn(params.pythonBin ?? resolveHermesStateDbRealtimePython(), ["-u", "-c", HERMES_STATE_DB_REALTIME_QUERY_SCRIPT, "serve", params.dbPath], {
|
|
256
|
-
cwd: homedir(),
|
|
257
|
-
env: { ...SUBPROCESS_ENV, ...process.env },
|
|
258
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
259
|
-
windowsHide: true,
|
|
260
|
-
});
|
|
261
|
-
child = started;
|
|
262
|
-
started.stdout.setEncoding("utf8");
|
|
263
|
-
started.stderr.setEncoding("utf8");
|
|
264
|
-
started.stdout.on("data", (chunk) => {
|
|
265
|
-
stdoutBuffer += chunk;
|
|
266
|
-
let newlineIndex = stdoutBuffer.indexOf("\n");
|
|
267
|
-
while (newlineIndex >= 0) {
|
|
268
|
-
const line = stdoutBuffer.slice(0, newlineIndex).trim();
|
|
269
|
-
stdoutBuffer = stdoutBuffer.slice(newlineIndex + 1);
|
|
270
|
-
if (line) {
|
|
271
|
-
let response;
|
|
272
|
-
try {
|
|
273
|
-
response = JSON.parse(line);
|
|
274
|
-
}
|
|
275
|
-
catch (error) {
|
|
276
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
277
|
-
stopChild(started, new Error(`Hermes state.db realtime server returned invalid JSON: ${detail}`));
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
const requestId = numberValue(response.id);
|
|
281
|
-
const request = requestId === undefined ? undefined : pending.get(requestId);
|
|
282
|
-
if (request) {
|
|
283
|
-
pending.delete(requestId);
|
|
284
|
-
clearTimeout(request.timeout);
|
|
285
|
-
if (response.ok === true) {
|
|
286
|
-
request.resolve(response.payload);
|
|
287
|
-
}
|
|
288
|
-
else {
|
|
289
|
-
request.reject(new Error(typeof response.error === "string" ? response.error : "Hermes state.db realtime query failed"));
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
newlineIndex = stdoutBuffer.indexOf("\n");
|
|
294
|
-
}
|
|
295
|
-
});
|
|
296
|
-
started.stderr.on("data", (chunk) => {
|
|
297
|
-
stderrTail = `${stderrTail}${chunk}`.slice(-8_192);
|
|
298
|
-
});
|
|
299
|
-
started.once("error", (error) => {
|
|
300
|
-
stopChild(started, error);
|
|
301
|
-
});
|
|
302
|
-
started.once("exit", (code, signal) => {
|
|
303
|
-
if (child === started) {
|
|
304
|
-
child = undefined;
|
|
305
|
-
}
|
|
306
|
-
if (pending.size > 0) {
|
|
307
|
-
const detail = stderrTail.trim();
|
|
308
|
-
rejectPending(new Error(`Hermes state.db realtime server exited (${signal ?? code ?? "unknown"})${detail ? `: ${detail}` : ""}`));
|
|
309
|
-
}
|
|
310
|
-
});
|
|
311
|
-
return started;
|
|
312
|
-
};
|
|
313
|
-
const query = async (mode, args) => {
|
|
314
|
-
const started = ensureChild();
|
|
315
|
-
const requestId = nextRequestId++;
|
|
316
|
-
return await new Promise((resolve, reject) => {
|
|
317
|
-
const timeout = setTimeout(() => {
|
|
318
|
-
pending.delete(requestId);
|
|
319
|
-
const error = new Error(`Hermes state.db realtime ${mode} query timed out after ${timeoutMs}ms`);
|
|
320
|
-
reject(error);
|
|
321
|
-
stopChild(started, error);
|
|
322
|
-
}, timeoutMs);
|
|
323
|
-
timeout.unref?.();
|
|
324
|
-
pending.set(requestId, { resolve, reject, timeout });
|
|
325
|
-
started.stdin.write(`${JSON.stringify({ id: requestId, mode, args })}\n`, (error) => {
|
|
326
|
-
if (!error) {
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
const request = pending.get(requestId);
|
|
330
|
-
if (!request) {
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
pending.delete(requestId);
|
|
334
|
-
clearTimeout(request.timeout);
|
|
335
|
-
request.reject(error);
|
|
336
|
-
stopChild(started, error);
|
|
337
|
-
});
|
|
338
|
-
});
|
|
339
|
-
};
|
|
340
|
-
return {
|
|
341
|
-
queryMaxMessageId: async () => {
|
|
342
|
-
const payload = await query("max", []);
|
|
343
|
-
return numberValue(payload) ?? 0;
|
|
344
|
-
},
|
|
345
|
-
queryRows: async ({ afterMessageId, limit }) => {
|
|
346
|
-
const payload = await query("rows", [
|
|
347
|
-
String(Math.max(0, Math.floor(afterMessageId))),
|
|
348
|
-
String(Math.max(1, Math.floor(limit ?? HERMES_STATE_DB_REALTIME_DEFAULT_LIMIT))),
|
|
349
|
-
]);
|
|
350
|
-
return Array.isArray(payload) ? payload.flatMap(parseHermesStateDbRealtimeMessageRow) : [];
|
|
351
|
-
},
|
|
352
|
-
queryOpenTurnRows: async ({ upToMessageId }) => {
|
|
353
|
-
const payload = await query("open_turn_users", [String(Math.max(0, Math.floor(upToMessageId)))]);
|
|
354
|
-
return Array.isArray(payload) ? payload.flatMap(parseHermesStateDbRealtimeMessageRow) : [];
|
|
355
|
-
},
|
|
356
|
-
close: () => {
|
|
357
|
-
closed = true;
|
|
358
|
-
const activeChild = child;
|
|
359
|
-
if (activeChild) {
|
|
360
|
-
stopChild(activeChild, new Error("Hermes state.db realtime query client closed"));
|
|
361
|
-
}
|
|
362
|
-
},
|
|
363
|
-
processId: () => child?.pid,
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
export function createHermesStateDbRealtimeWatcher(params) {
|
|
367
|
-
let cursor = { lastMessageId: 0, openTurnsBySession: {} };
|
|
368
|
-
let timer;
|
|
369
|
-
let running = false;
|
|
370
|
-
let primed = false;
|
|
371
|
-
let startGeneration = 0;
|
|
372
|
-
let queryClient;
|
|
373
|
-
const pollIntervalMs = Math.max(250, Math.floor(params.pollIntervalMs ?? HERMES_STATE_DB_REALTIME_DEFAULT_POLL_INTERVAL_MS));
|
|
374
|
-
const ensureQueryClient = () => {
|
|
375
|
-
queryClient ??= params.queryClientFactory?.()
|
|
376
|
-
?? createHermesStateDbRealtimeQueryClient({ dbPath: params.dbPath });
|
|
377
|
-
return queryClient;
|
|
378
|
-
};
|
|
379
|
-
const queryMaxMessageId = params.queryMaxMessageId
|
|
380
|
-
?? (() => existsSync(params.dbPath) ? ensureQueryClient().queryMaxMessageId() : Promise.resolve(0));
|
|
381
|
-
const queryRows = params.queryRows
|
|
382
|
-
?? ((queryParams) => existsSync(params.dbPath)
|
|
383
|
-
? ensureQueryClient().queryRows(queryParams)
|
|
384
|
-
: Promise.resolve([]));
|
|
385
|
-
const queryOpenTurnRows = params.queryOpenTurnRows
|
|
386
|
-
?? ((queryParams) => existsSync(params.dbPath)
|
|
387
|
-
? ensureQueryClient().queryOpenTurnRows(queryParams)
|
|
388
|
-
: Promise.resolve([]));
|
|
389
|
-
async function withErrorBoundary(operation) {
|
|
390
|
-
try {
|
|
391
|
-
await operation();
|
|
392
|
-
}
|
|
393
|
-
catch (error) {
|
|
394
|
-
params.onError?.(error);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
async function tryPrimeCursor() {
|
|
398
|
-
try {
|
|
399
|
-
const maxMessageId = await queryMaxMessageId();
|
|
400
|
-
const openTurnRows = await queryOpenTurnRows({ upToMessageId: maxMessageId });
|
|
401
|
-
const restored = restoreHermesStateDbRealtimeOpenTurns(params.gatewayId, cursor, openTurnRows);
|
|
402
|
-
cursor = {
|
|
403
|
-
...restored.cursor,
|
|
404
|
-
lastMessageId: Math.max(cursor.lastMessageId, maxMessageId),
|
|
405
|
-
};
|
|
406
|
-
for (const payload of restored.payloads) {
|
|
407
|
-
params.publishPayload(payload);
|
|
408
|
-
}
|
|
409
|
-
return true;
|
|
410
|
-
}
|
|
411
|
-
catch (error) {
|
|
412
|
-
params.onError?.(error);
|
|
413
|
-
return false;
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
async function primeCursor() {
|
|
417
|
-
primed = await tryPrimeCursor();
|
|
418
|
-
}
|
|
419
|
-
async function pollOnce(expectedStartGeneration) {
|
|
420
|
-
if (running) {
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
running = true;
|
|
424
|
-
try {
|
|
425
|
-
if (!primed) {
|
|
426
|
-
primed = await tryPrimeCursor();
|
|
427
|
-
if (!primed) {
|
|
428
|
-
return;
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
if (expectedStartGeneration !== undefined && expectedStartGeneration !== startGeneration) {
|
|
432
|
-
return;
|
|
433
|
-
}
|
|
434
|
-
await withErrorBoundary(async () => {
|
|
435
|
-
const rows = await queryRows({ afterMessageId: cursor.lastMessageId });
|
|
436
|
-
if (rows.length === 0
|
|
437
|
-
|| (expectedStartGeneration !== undefined && expectedStartGeneration !== startGeneration)) {
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
|
-
const result = buildHermesStateDbRealtimePayloads({
|
|
441
|
-
gatewayId: params.gatewayId,
|
|
442
|
-
cursor,
|
|
443
|
-
rows,
|
|
444
|
-
});
|
|
445
|
-
cursor = result.cursor;
|
|
446
|
-
for (const payload of result.payloads) {
|
|
447
|
-
params.publishPayload(payload);
|
|
448
|
-
}
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
finally {
|
|
452
|
-
running = false;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
function start() {
|
|
456
|
-
if (timer) {
|
|
457
|
-
return;
|
|
458
|
-
}
|
|
459
|
-
const generation = ++startGeneration;
|
|
460
|
-
void pollOnce(generation);
|
|
461
|
-
timer = setInterval(() => {
|
|
462
|
-
void pollOnce(generation);
|
|
463
|
-
}, pollIntervalMs);
|
|
464
|
-
}
|
|
465
|
-
function stop() {
|
|
466
|
-
startGeneration += 1;
|
|
467
|
-
if (timer) {
|
|
468
|
-
clearInterval(timer);
|
|
469
|
-
timer = undefined;
|
|
470
|
-
}
|
|
471
|
-
queryClient?.close();
|
|
472
|
-
queryClient = undefined;
|
|
473
|
-
primed = false;
|
|
474
|
-
}
|
|
475
|
-
return {
|
|
476
|
-
primeCursor,
|
|
477
|
-
pollOnce,
|
|
478
|
-
start,
|
|
479
|
-
stop,
|
|
480
|
-
cursor: () => cloneCursor(cursor),
|
|
481
|
-
};
|
|
482
|
-
}
|
|
483
|
-
export function resolveHermesStateDbRealtimePath() {
|
|
484
|
-
return resolveHermesStateDbPath();
|
|
485
|
-
}
|
|
486
|
-
export function resolveHermesStateDbRealtimePython() {
|
|
487
|
-
return resolveHermesPythonBin();
|
|
488
|
-
}
|
|
489
|
-
async function runHermesStateDbRealtimeQuery(mode, dbPath, args, pythonBin = resolveHermesStateDbRealtimePython()) {
|
|
490
|
-
return readHermesStateDbRealtimePayload(async () => {
|
|
491
|
-
const { stdout } = await execFile(pythonBin, ["-c", HERMES_STATE_DB_REALTIME_QUERY_SCRIPT, mode, dbPath, ...args], {
|
|
492
|
-
cwd: homedir(),
|
|
493
|
-
encoding: "utf8",
|
|
494
|
-
env: { ...SUBPROCESS_ENV, ...process.env },
|
|
495
|
-
timeout: HERMES_STATE_DB_REALTIME_QUERY_TIMEOUT_MS,
|
|
496
|
-
maxBuffer: 16 * 1024 * 1024,
|
|
497
|
-
windowsHide: true,
|
|
498
|
-
});
|
|
499
|
-
return stdout;
|
|
500
|
-
});
|
|
501
|
-
}
|
|
502
|
-
class HermesStateDbRealtimeOutputError extends Error {
|
|
503
|
-
}
|
|
504
|
-
export async function readHermesStateDbRealtimePayload(readOutput, maxAttempts = HERMES_STATE_DB_REALTIME_QUERY_ATTEMPTS) {
|
|
505
|
-
const attempts = Math.max(1, Math.floor(maxAttempts));
|
|
506
|
-
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
507
|
-
let parsed;
|
|
508
|
-
try {
|
|
509
|
-
const output = (await readOutput()).trim();
|
|
510
|
-
if (!output) {
|
|
511
|
-
throw new HermesStateDbRealtimeOutputError("Hermes state.db realtime query returned empty output");
|
|
512
|
-
}
|
|
513
|
-
try {
|
|
514
|
-
parsed = JSON.parse(output);
|
|
515
|
-
}
|
|
516
|
-
catch (error) {
|
|
517
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
518
|
-
throw new HermesStateDbRealtimeOutputError(`Hermes state.db realtime query returned invalid JSON: ${detail}`);
|
|
519
|
-
}
|
|
520
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
521
|
-
throw new HermesStateDbRealtimeOutputError("Hermes state.db realtime query returned an invalid payload");
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
catch (error) {
|
|
525
|
-
if (error instanceof HermesStateDbRealtimeOutputError && attempt < attempts) {
|
|
526
|
-
continue;
|
|
527
|
-
}
|
|
528
|
-
if (error instanceof HermesStateDbRealtimeOutputError && attempts > 1) {
|
|
529
|
-
throw new HermesStateDbRealtimeOutputError(`${error.message} after ${attempts} attempts`);
|
|
530
|
-
}
|
|
531
|
-
throw error;
|
|
532
|
-
}
|
|
533
|
-
const result = parsed;
|
|
534
|
-
if (result.ok === true) {
|
|
535
|
-
return result.payload;
|
|
536
|
-
}
|
|
537
|
-
throw new Error(typeof result.error === "string" ? result.error : "Hermes state.db realtime query failed");
|
|
538
|
-
}
|
|
539
|
-
throw new Error("Hermes state.db realtime query failed");
|
|
540
|
-
}
|
|
541
|
-
function parseHermesStateDbRealtimeMessageRow(value) {
|
|
542
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
543
|
-
return [];
|
|
544
|
-
}
|
|
545
|
-
const record = value;
|
|
546
|
-
const id = numberValue(record.id);
|
|
547
|
-
const sessionId = stringValue(record.sessionId);
|
|
548
|
-
const role = parseTimelineRole(record.role);
|
|
549
|
-
const timestamp = numberValue(record.timestamp);
|
|
550
|
-
if (id === undefined || !sessionId || !role || timestamp === undefined) {
|
|
551
|
-
return [];
|
|
552
|
-
}
|
|
553
|
-
return [{
|
|
554
|
-
id,
|
|
555
|
-
sessionId,
|
|
556
|
-
sessionSource: stringValue(record.sessionSource) ?? "",
|
|
557
|
-
role,
|
|
558
|
-
content: stringValue(record.content) ?? "",
|
|
559
|
-
timestamp,
|
|
560
|
-
active: booleanValue(record.active, true),
|
|
561
|
-
observed: booleanValue(record.observed, false),
|
|
562
|
-
...(stringValue(record.toolName) ? { toolName: stringValue(record.toolName) } : {}),
|
|
563
|
-
...(stringValue(record.toolCallId) ? { toolCallId: stringValue(record.toolCallId) } : {}),
|
|
564
|
-
...(stringValue(record.finishReason) ? { finishReason: stringValue(record.finishReason) } : {}),
|
|
565
|
-
...(stringValue(record.toolCalls) ? { toolCalls: stringValue(record.toolCalls) } : {}),
|
|
566
|
-
...(stringValue(record.platformMessageId) ? { platformMessageId: stringValue(record.platformMessageId) } : {}),
|
|
567
|
-
}];
|
|
568
|
-
}
|
|
569
|
-
function parseTimelineRole(value) {
|
|
570
|
-
const role = stringValue(value)?.trim().toLowerCase();
|
|
571
|
-
return role === "user" || role === "assistant" || role === "tool" || role === "system"
|
|
572
|
-
? role
|
|
573
|
-
: undefined;
|
|
574
|
-
}
|
|
575
|
-
function stringValue(value) {
|
|
576
|
-
if (typeof value === "string") {
|
|
577
|
-
return value;
|
|
578
|
-
}
|
|
579
|
-
if (typeof value === "number" || typeof value === "boolean") {
|
|
580
|
-
return String(value);
|
|
581
|
-
}
|
|
582
|
-
return undefined;
|
|
583
|
-
}
|
|
584
|
-
function numberValue(value) {
|
|
585
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
586
|
-
return value;
|
|
587
|
-
}
|
|
588
|
-
if (typeof value === "string" && value.trim()) {
|
|
589
|
-
const parsed = Number(value);
|
|
590
|
-
return Number.isFinite(parsed) ? parsed : undefined;
|
|
591
|
-
}
|
|
592
|
-
return undefined;
|
|
593
|
-
}
|
|
594
|
-
function booleanValue(value, fallback) {
|
|
595
|
-
if (typeof value === "boolean") {
|
|
596
|
-
return value;
|
|
597
|
-
}
|
|
598
|
-
if (typeof value === "number") {
|
|
599
|
-
return value !== 0;
|
|
600
|
-
}
|
|
601
|
-
if (typeof value === "string") {
|
|
602
|
-
const normalized = value.trim().toLowerCase();
|
|
603
|
-
if (normalized === "true" || normalized === "1") {
|
|
604
|
-
return true;
|
|
605
|
-
}
|
|
606
|
-
if (normalized === "false" || normalized === "0") {
|
|
607
|
-
return false;
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
return fallback;
|
|
611
|
-
}
|
|
612
|
-
function cloneCursor(cursor) {
|
|
613
|
-
return {
|
|
614
|
-
lastMessageId: cursor.lastMessageId,
|
|
615
|
-
openTurnsBySession: Object.fromEntries(Object.entries(cursor.openTurnsBySession).map(([key, value]) => [key, { ...value }])),
|
|
616
|
-
};
|
|
617
|
-
}
|
|
618
|
-
function restoreHermesStateDbRealtimeOpenTurns(gatewayId, cursor, rows) {
|
|
619
|
-
const restored = {
|
|
620
|
-
lastMessageId: cursor.lastMessageId,
|
|
621
|
-
openTurnsBySession: {},
|
|
622
|
-
};
|
|
623
|
-
const latestMobileFinalBySession = new Map();
|
|
624
|
-
for (const row of [...rows].sort((left, right) => left.id - right.id)) {
|
|
625
|
-
if (!row.active) {
|
|
626
|
-
continue;
|
|
627
|
-
}
|
|
628
|
-
if (row.role === "user") {
|
|
629
|
-
const mobileTurn = isClawConnectMobileTurn(row.content);
|
|
630
|
-
const visibleText = normalizeHermesStateDbUserText(row.content);
|
|
631
|
-
if (!visibleText && !mobileTurn) {
|
|
632
|
-
delete restored.openTurnsBySession[row.sessionId];
|
|
633
|
-
latestMobileFinalBySession.delete(row.sessionId);
|
|
634
|
-
continue;
|
|
635
|
-
}
|
|
636
|
-
restored.openTurnsBySession[row.sessionId] = buildOpenTurn(row, mobileTurn);
|
|
637
|
-
continue;
|
|
638
|
-
}
|
|
639
|
-
const turn = restored.openTurnsBySession[row.sessionId];
|
|
640
|
-
if (!turn?.mobileTurn || !isTerminalHermesStateDbAssistant(row)) {
|
|
641
|
-
continue;
|
|
642
|
-
}
|
|
643
|
-
const visibleText = normalizeHermesStateDbOutputText(row.content);
|
|
644
|
-
if (!visibleText) {
|
|
645
|
-
continue;
|
|
646
|
-
}
|
|
647
|
-
latestMobileFinalBySession.set(row.sessionId, { row, turn });
|
|
648
|
-
}
|
|
649
|
-
const payloads = [...latestMobileFinalBySession.values()]
|
|
650
|
-
.sort((left, right) => left.row.id - right.row.id)
|
|
651
|
-
.map(({ row, turn }) => buildOutputPayload(gatewayId, row, turn, normalizeHermesStateDbOutputText(row.content)));
|
|
652
|
-
return { cursor: restored, payloads };
|
|
653
|
-
}
|
|
654
|
-
function buildOpenTurn(row, mobileTurn) {
|
|
655
|
-
const mobileSourceRunId = row.role === "user" ? clawConnectMobileSourceRunId(row.content) : undefined;
|
|
656
|
-
const mobileSessionKey = row.role === "user" ? clawConnectMobileSessionKey(row.content) : undefined;
|
|
657
|
-
const turnId = mobileSourceRunId
|
|
658
|
-
?? (row.role === "user"
|
|
659
|
-
? `hermes-db-${row.sessionId}-turn-${row.id}`
|
|
660
|
-
: `hermes-db-${row.sessionId}-orphan-turn-${row.id}`);
|
|
661
|
-
return {
|
|
662
|
-
turnId,
|
|
663
|
-
runId: turnId,
|
|
664
|
-
mobileTurn,
|
|
665
|
-
...(mobileSessionKey ? { sessionKey: mobileSessionKey } : {}),
|
|
666
|
-
};
|
|
667
|
-
}
|
|
668
|
-
function buildUserPayload(gatewayId, row, turn, text) {
|
|
669
|
-
const event = buildTimelineEvent({
|
|
670
|
-
gatewayId,
|
|
671
|
-
sessionKey: hermesStateDbTurnSessionKey(row.sessionId, turn),
|
|
672
|
-
row,
|
|
673
|
-
turn,
|
|
674
|
-
eventType: "turn.user.created",
|
|
675
|
-
role: "user",
|
|
676
|
-
messageState: "completed",
|
|
677
|
-
runState: "active",
|
|
678
|
-
content: [{ type: "text", text }],
|
|
679
|
-
});
|
|
680
|
-
return {
|
|
681
|
-
runId: turn.runId,
|
|
682
|
-
sessionKey: event.sessionKey,
|
|
683
|
-
state: "history_sync",
|
|
684
|
-
role: "user",
|
|
685
|
-
timelineEvents: [event],
|
|
686
|
-
};
|
|
687
|
-
}
|
|
688
|
-
function buildOutputPayload(gatewayId, row, turn, text) {
|
|
689
|
-
const completed = buildTimelineEvent({
|
|
690
|
-
gatewayId,
|
|
691
|
-
sessionKey: hermesStateDbTurnSessionKey(row.sessionId, turn),
|
|
692
|
-
row,
|
|
693
|
-
turn,
|
|
694
|
-
eventType: "message.completed",
|
|
695
|
-
role: row.role,
|
|
696
|
-
messageState: "completed",
|
|
697
|
-
runState: "active",
|
|
698
|
-
content: [{ type: "text", text }],
|
|
699
|
-
...(turn.mobileTurn && row.role === "assistant"
|
|
700
|
-
? { messageId: `assistant-${turn.runId}` }
|
|
701
|
-
: {}),
|
|
702
|
-
});
|
|
703
|
-
const runCompleted = buildTimelineEvent({
|
|
704
|
-
gatewayId,
|
|
705
|
-
sessionKey: completed.sessionKey,
|
|
706
|
-
row,
|
|
707
|
-
turn,
|
|
708
|
-
eventType: "run.completed",
|
|
709
|
-
role: row.role,
|
|
710
|
-
messageState: "completed",
|
|
711
|
-
runState: "completed",
|
|
712
|
-
content: [],
|
|
713
|
-
messageId: completed.messageId,
|
|
714
|
-
partId: "run-state",
|
|
715
|
-
});
|
|
716
|
-
return {
|
|
717
|
-
runId: turn.runId,
|
|
718
|
-
sessionKey: completed.sessionKey,
|
|
719
|
-
state: "history_sync",
|
|
720
|
-
role: row.role,
|
|
721
|
-
timelineEvents: [completed, runCompleted],
|
|
722
|
-
};
|
|
723
|
-
}
|
|
724
|
-
function buildTimelineEvent(params) {
|
|
725
|
-
const messageId = params.messageId ?? hermesStateDbMessageId({
|
|
726
|
-
sessionId: params.row.sessionId,
|
|
727
|
-
rowId: params.row.id,
|
|
728
|
-
role: params.row.role,
|
|
729
|
-
});
|
|
730
|
-
return parseCanonicalTimelineEvent({
|
|
731
|
-
protocolVersion: 2,
|
|
732
|
-
eventId: `evt-${messageId}-${params.eventType}`,
|
|
733
|
-
eventType: params.eventType,
|
|
734
|
-
gatewayId: params.gatewayId,
|
|
735
|
-
sessionKey: params.sessionKey,
|
|
736
|
-
turnId: params.turn.turnId,
|
|
737
|
-
runId: params.turn.runId,
|
|
738
|
-
messageId,
|
|
739
|
-
partId: params.partId ?? "part-text-1",
|
|
740
|
-
attachmentId: null,
|
|
741
|
-
seq: params.row.id,
|
|
742
|
-
turnSeq: params.row.id,
|
|
743
|
-
role: params.role,
|
|
744
|
-
messageState: params.messageState,
|
|
745
|
-
runState: params.runState,
|
|
746
|
-
createdAt: timestampToIso(params.row.timestamp),
|
|
747
|
-
source: "history",
|
|
748
|
-
content: params.content,
|
|
749
|
-
attachment: null,
|
|
750
|
-
error: null,
|
|
751
|
-
});
|
|
752
|
-
}
|
|
753
|
-
function hermesStateDbSessionKey(sessionId) {
|
|
754
|
-
return sessionId.startsWith("hermes:") ? sessionId : `hermes:${sessionId}`;
|
|
755
|
-
}
|
|
756
|
-
function hermesStateDbTurnSessionKey(sessionId, turn) {
|
|
757
|
-
return turn.sessionKey?.trim() || hermesStateDbSessionKey(sessionId);
|
|
758
|
-
}
|
|
759
|
-
function normalizeHermesStateDbUserText(text) {
|
|
760
|
-
return stripClawConnectMetadata(text)
|
|
761
|
-
.split(/\r?\n/)
|
|
762
|
-
.filter((line) => !/^\s*\[file attached:\s+.+]\s*$/i.test(line.trim()))
|
|
763
|
-
.join("\n")
|
|
764
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
765
|
-
.trim();
|
|
766
|
-
}
|
|
767
|
-
function normalizeHermesStateDbOutputText(text) {
|
|
768
|
-
return canonicalizeMobileAssistantText(text).text.trim();
|
|
769
|
-
}
|
|
770
|
-
function stripClawConnectMetadata(text) {
|
|
771
|
-
const withoutRuntimeContext = text
|
|
772
|
-
.replace(HERMES_RUNTIME_CONTEXT_HINT_REGEX, "$1")
|
|
773
|
-
.replace(/\n{3,}/g, "\n\n");
|
|
774
|
-
const cutoff = [
|
|
775
|
-
withoutRuntimeContext.indexOf(CLAWCONNECT_MOBILE_BRIDGE_MARKER),
|
|
776
|
-
withoutRuntimeContext.indexOf(CLAWCONNECT_MOBILE_TURN_MARKER),
|
|
777
|
-
]
|
|
778
|
-
.filter((index) => index >= 0)
|
|
779
|
-
.sort((left, right) => left - right)[0];
|
|
780
|
-
return cutoff === undefined ? withoutRuntimeContext : withoutRuntimeContext.slice(0, cutoff);
|
|
781
|
-
}
|
|
782
|
-
function isClawConnectMobileTurn(text) {
|
|
783
|
-
return text.includes(CLAWCONNECT_MOBILE_TURN_MARKER);
|
|
784
|
-
}
|
|
785
|
-
function clawConnectMobileSourceRunId(text) {
|
|
786
|
-
return clawConnectMobileMetadataValue(text, "sourceRunId");
|
|
787
|
-
}
|
|
788
|
-
function clawConnectMobileSessionKey(text) {
|
|
789
|
-
return clawConnectMobileMetadataValue(text, "sessionKey");
|
|
790
|
-
}
|
|
791
|
-
function clawConnectMobileMetadataValue(text, key) {
|
|
792
|
-
const markerIndex = text.indexOf(CLAWCONNECT_MOBILE_TURN_MARKER);
|
|
793
|
-
if (markerIndex < 0) {
|
|
794
|
-
return undefined;
|
|
795
|
-
}
|
|
796
|
-
const metadataBlock = text.slice(markerIndex + CLAWCONNECT_MOBILE_TURN_MARKER.length);
|
|
797
|
-
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
798
|
-
const match = new RegExp(`^\\s*${escapedKey}\\s*:\\s*(.+?)\\s*$`, "im").exec(metadataBlock);
|
|
799
|
-
return match?.[1]?.trim() || undefined;
|
|
800
|
-
}
|
|
801
|
-
function isTerminalHermesStateDbAssistant(row) {
|
|
802
|
-
if (row.role !== "assistant") {
|
|
803
|
-
return false;
|
|
804
|
-
}
|
|
805
|
-
const finishReason = row.finishReason?.trim().toLowerCase().replace(/[\s-]+/g, "_") ?? "";
|
|
806
|
-
if (finishReason === "tool_calls" || finishReason === "function_call") {
|
|
807
|
-
return false;
|
|
808
|
-
}
|
|
809
|
-
return !hasHermesStateDbToolCalls(row.toolCalls);
|
|
810
|
-
}
|
|
811
|
-
function hasHermesStateDbToolCalls(value) {
|
|
812
|
-
const trimmed = value?.trim();
|
|
813
|
-
if (!trimmed || trimmed === "[]" || trimmed === "{}" || trimmed === "null") {
|
|
814
|
-
return false;
|
|
815
|
-
}
|
|
816
|
-
try {
|
|
817
|
-
const parsed = JSON.parse(trimmed);
|
|
818
|
-
if (Array.isArray(parsed)) {
|
|
819
|
-
return parsed.length > 0;
|
|
820
|
-
}
|
|
821
|
-
if (parsed && typeof parsed === "object") {
|
|
822
|
-
return Object.keys(parsed).length > 0;
|
|
823
|
-
}
|
|
824
|
-
return parsed !== null;
|
|
825
|
-
}
|
|
826
|
-
catch {
|
|
827
|
-
return true;
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
function timestampToIso(timestamp) {
|
|
831
|
-
return new Date(timestamp < 10_000_000_000 ? timestamp * 1_000 : timestamp).toISOString();
|
|
832
|
-
}
|
|
833
|
-
//# sourceMappingURL=hermes-state-db-realtime.js.map
|