clawconnect-agent 0.1.32 → 0.1.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/relay/timeline-event-builder.js +3 -1
- package/dist/core/relay/timeline-event-builder.js.map +1 -1
- package/dist/hermes/hermes-runtime.d.ts +1 -1
- package/dist/hermes/hermes-runtime.js +1 -1
- package/dist/hermes/hermes-runtime.js.map +1 -1
- package/dist/hermes/relay/hermes-state-db-realtime.d.ts +30 -1
- package/dist/hermes/relay/hermes-state-db-realtime.js +439 -72
- package/dist/hermes/relay/hermes-state-db-realtime.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-api-client.d.ts +14 -1
- package/dist/hermes/runtime/hermes-runtime-api-client.js +155 -26
- package/dist/hermes/runtime/hermes-runtime-api-client.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-chat.d.ts +1 -0
- package/dist/hermes/runtime/hermes-runtime-chat.js +1 -0
- package/dist/hermes/runtime/hermes-runtime-chat.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-command-router.js +5 -2
- package/dist/hermes/runtime/hermes-runtime-command-router.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-history-completion.d.ts +2 -0
- package/dist/hermes/runtime/hermes-runtime-history-completion.js +49 -2
- package/dist/hermes/runtime/hermes-runtime-history-completion.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-history.js +82 -9
- package/dist/hermes/runtime/hermes-runtime-history.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-process.js +14 -2
- package/dist/hermes/runtime/hermes-runtime-process.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-sessions.d.ts +5 -0
- package/dist/hermes/runtime/hermes-runtime-sessions.js +38 -6
- package/dist/hermes/runtime/hermes-runtime-sessions.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-state-db.d.ts +14 -0
- package/dist/hermes/runtime/hermes-runtime-state-db.js +116 -0
- package/dist/hermes/runtime/hermes-runtime-state-db.js.map +1 -1
- package/dist/hermes/runtime/hermes-state-db-message-identity.d.ts +12 -0
- package/dist/hermes/runtime/hermes-state-db-message-identity.js +11 -0
- package/dist/hermes/runtime/hermes-state-db-message-identity.js.map +1 -0
- package/dist/openclaw/gateway-client.d.ts +3 -1
- package/dist/openclaw/gateway-client.js +16 -4
- package/dist/openclaw/gateway-client.js.map +1 -1
- package/dist/openclaw/relay/chat-history.d.ts +2 -1
- package/dist/openclaw/relay/chat-history.js +26 -13
- package/dist/openclaw/relay/chat-history.js.map +1 -1
- package/dist/openclaw/relay/chat-run-identity.d.ts +25 -0
- package/dist/openclaw/relay/chat-run-identity.js +81 -0
- package/dist/openclaw/relay/chat-run-identity.js.map +1 -0
- package/dist/openclaw/relay-manager.js +128 -73
- package/dist/openclaw/relay-manager.js.map +1 -1
- package/dist/openclaw/runtime/local-runtime.js +4 -1
- package/dist/openclaw/runtime/local-runtime.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execFile as execFileCb } from "node:child_process";
|
|
1
|
+
import { execFile as execFileCb, spawn } from "node:child_process";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { promisify } from "node:util";
|
|
@@ -6,6 +6,7 @@ import { canonicalizeMobileAssistantText } from "../../core/relay/mobile-chat-ru
|
|
|
6
6
|
import { parseCanonicalTimelineEvent, } from "../../core/relay/timeline-event-log.js";
|
|
7
7
|
import { SUBPROCESS_ENV, } from "../runtime/hermes-runtime-process.js";
|
|
8
8
|
import { resolveHermesPythonBin, resolveHermesStateDbPath } from "../runtime/hermes-runtime-paths.js";
|
|
9
|
+
import { hermesStateDbMessageId } from "../runtime/hermes-state-db-message-identity.js";
|
|
9
10
|
const execFile = promisify(execFileCb);
|
|
10
11
|
const CLAWCONNECT_MOBILE_TURN_MARKER = "[ClawConnect mobile turn]";
|
|
11
12
|
const CLAWCONNECT_MOBILE_BRIDGE_MARKER = "[ClawConnect mobile bridge]";
|
|
@@ -19,7 +20,6 @@ import json
|
|
|
19
20
|
import sqlite3
|
|
20
21
|
import sys
|
|
21
22
|
|
|
22
|
-
mode = sys.argv[1]
|
|
23
23
|
db_path = sys.argv[2]
|
|
24
24
|
|
|
25
25
|
def connect():
|
|
@@ -28,14 +28,30 @@ def connect():
|
|
|
28
28
|
conn.execute("PRAGMA query_only=ON")
|
|
29
29
|
return conn
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
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):
|
|
33
49
|
if mode == "max":
|
|
34
50
|
row = conn.execute("SELECT COALESCE(MAX(id), 0) AS max_id FROM messages").fetchone()
|
|
35
|
-
|
|
51
|
+
return int(row["max_id"] or 0)
|
|
36
52
|
elif mode == "rows":
|
|
37
|
-
after_id = int(
|
|
38
|
-
limit = int(
|
|
53
|
+
after_id = int(args[0])
|
|
54
|
+
limit = int(args[1])
|
|
39
55
|
rows = conn.execute("""
|
|
40
56
|
SELECT
|
|
41
57
|
m.id,
|
|
@@ -48,6 +64,8 @@ try:
|
|
|
48
64
|
COALESCE(m.observed, 0) AS observed,
|
|
49
65
|
m.tool_name,
|
|
50
66
|
m.tool_call_id,
|
|
67
|
+
m.finish_reason,
|
|
68
|
+
m.tool_calls,
|
|
51
69
|
m.platform_message_id
|
|
52
70
|
FROM messages m
|
|
53
71
|
LEFT JOIN sessions s ON s.id = m.session_id
|
|
@@ -55,26 +73,84 @@ try:
|
|
|
55
73
|
ORDER BY m.id ASC
|
|
56
74
|
LIMIT ?
|
|
57
75
|
""", (after_id, limit)).fetchall()
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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))
|
|
78
154
|
`;
|
|
79
155
|
export function buildHermesStateDbRealtimePayloads(params) {
|
|
80
156
|
const cursor = cloneCursor(params.cursor);
|
|
@@ -94,19 +170,19 @@ export function buildHermesStateDbRealtimePayloads(params) {
|
|
|
94
170
|
delete cursor.openTurnsBySession[row.sessionId];
|
|
95
171
|
continue;
|
|
96
172
|
}
|
|
97
|
-
//
|
|
98
|
-
// state.db
|
|
99
|
-
const
|
|
100
|
-
const turn = buildOpenTurn(row,
|
|
173
|
+
// 移动端 user 已由 Relay 接收,不重复回写;但最终 assistant 必须从
|
|
174
|
+
// state.db 用相同 runId 回补,防止 live 断线或错误的中间完成让刷新丢回答。
|
|
175
|
+
const mobileTurn = isClawConnectMobileTurn(row.content);
|
|
176
|
+
const turn = buildOpenTurn(row, mobileTurn);
|
|
101
177
|
cursor.openTurnsBySession[row.sessionId] = turn;
|
|
102
|
-
if (
|
|
178
|
+
if (mobileTurn) {
|
|
103
179
|
continue;
|
|
104
180
|
}
|
|
105
181
|
payloads.push(buildUserPayload(params.gatewayId, row, turn, visibleText));
|
|
106
182
|
continue;
|
|
107
183
|
}
|
|
108
184
|
const turn = cursor.openTurnsBySession[row.sessionId] ?? buildOpenTurn(row, false);
|
|
109
|
-
if (turn.
|
|
185
|
+
if (turn.mobileTurn && !isTerminalHermesStateDbAssistant(row)) {
|
|
110
186
|
continue;
|
|
111
187
|
}
|
|
112
188
|
const visibleText = normalizeHermesStateDbOutputText(row.content);
|
|
@@ -133,18 +209,183 @@ export async function queryHermesStateDbRealtimeRows(params) {
|
|
|
133
209
|
const payload = await runHermesStateDbRealtimeQuery("rows", params.dbPath, [String(afterMessageId), String(limit)], params.pythonBin);
|
|
134
210
|
return Array.isArray(payload) ? payload.flatMap(parseHermesStateDbRealtimeMessageRow) : [];
|
|
135
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
|
+
}
|
|
136
366
|
export function createHermesStateDbRealtimeWatcher(params) {
|
|
137
367
|
let cursor = { lastMessageId: 0, openTurnsBySession: {} };
|
|
138
368
|
let timer;
|
|
139
369
|
let running = false;
|
|
370
|
+
let primed = false;
|
|
371
|
+
let startGeneration = 0;
|
|
372
|
+
let queryClient;
|
|
140
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
|
+
};
|
|
141
379
|
const queryMaxMessageId = params.queryMaxMessageId
|
|
142
|
-
?? (() =>
|
|
380
|
+
?? (() => existsSync(params.dbPath) ? ensureQueryClient().queryMaxMessageId() : Promise.resolve(0));
|
|
143
381
|
const queryRows = params.queryRows
|
|
144
|
-
?? ((queryParams) =>
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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([]));
|
|
148
389
|
async function withErrorBoundary(operation) {
|
|
149
390
|
try {
|
|
150
391
|
await operation();
|
|
@@ -153,52 +394,83 @@ export function createHermesStateDbRealtimeWatcher(params) {
|
|
|
153
394
|
params.onError?.(error);
|
|
154
395
|
}
|
|
155
396
|
}
|
|
156
|
-
async function
|
|
157
|
-
|
|
397
|
+
async function tryPrimeCursor() {
|
|
398
|
+
try {
|
|
158
399
|
const maxMessageId = await queryMaxMessageId();
|
|
400
|
+
const openTurnRows = await queryOpenTurnRows({ upToMessageId: maxMessageId });
|
|
401
|
+
const restored = restoreHermesStateDbRealtimeOpenTurns(params.gatewayId, cursor, openTurnRows);
|
|
159
402
|
cursor = {
|
|
160
|
-
...cursor,
|
|
403
|
+
...restored.cursor,
|
|
161
404
|
lastMessageId: Math.max(cursor.lastMessageId, maxMessageId),
|
|
162
405
|
};
|
|
163
|
-
|
|
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
|
+
}
|
|
164
415
|
}
|
|
165
|
-
async function
|
|
416
|
+
async function primeCursor() {
|
|
417
|
+
primed = await tryPrimeCursor();
|
|
418
|
+
}
|
|
419
|
+
async function pollOnce(expectedStartGeneration) {
|
|
166
420
|
if (running) {
|
|
167
421
|
return;
|
|
168
422
|
}
|
|
169
423
|
running = true;
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
424
|
+
try {
|
|
425
|
+
if (!primed) {
|
|
426
|
+
primed = await tryPrimeCursor();
|
|
427
|
+
if (!primed) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (expectedStartGeneration !== undefined && expectedStartGeneration !== startGeneration) {
|
|
173
432
|
return;
|
|
174
433
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
+
}
|
|
179
449
|
});
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
});
|
|
185
|
-
running = false;
|
|
450
|
+
}
|
|
451
|
+
finally {
|
|
452
|
+
running = false;
|
|
453
|
+
}
|
|
186
454
|
}
|
|
187
455
|
function start() {
|
|
188
456
|
if (timer) {
|
|
189
457
|
return;
|
|
190
458
|
}
|
|
191
|
-
|
|
459
|
+
const generation = ++startGeneration;
|
|
460
|
+
void pollOnce(generation);
|
|
192
461
|
timer = setInterval(() => {
|
|
193
|
-
void pollOnce();
|
|
462
|
+
void pollOnce(generation);
|
|
194
463
|
}, pollIntervalMs);
|
|
195
464
|
}
|
|
196
465
|
function stop() {
|
|
197
|
-
|
|
198
|
-
|
|
466
|
+
startGeneration += 1;
|
|
467
|
+
if (timer) {
|
|
468
|
+
clearInterval(timer);
|
|
469
|
+
timer = undefined;
|
|
199
470
|
}
|
|
200
|
-
|
|
201
|
-
|
|
471
|
+
queryClient?.close();
|
|
472
|
+
queryClient = undefined;
|
|
473
|
+
primed = false;
|
|
202
474
|
}
|
|
203
475
|
return {
|
|
204
476
|
primeCursor,
|
|
@@ -221,6 +493,7 @@ async function runHermesStateDbRealtimeQuery(mode, dbPath, args, pythonBin = res
|
|
|
221
493
|
encoding: "utf8",
|
|
222
494
|
env: { ...SUBPROCESS_ENV, ...process.env },
|
|
223
495
|
timeout: HERMES_STATE_DB_REALTIME_QUERY_TIMEOUT_MS,
|
|
496
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
224
497
|
windowsHide: true,
|
|
225
498
|
});
|
|
226
499
|
return stdout;
|
|
@@ -288,6 +561,8 @@ function parseHermesStateDbRealtimeMessageRow(value) {
|
|
|
288
561
|
observed: booleanValue(record.observed, false),
|
|
289
562
|
...(stringValue(record.toolName) ? { toolName: stringValue(record.toolName) } : {}),
|
|
290
563
|
...(stringValue(record.toolCallId) ? { toolCallId: stringValue(record.toolCallId) } : {}),
|
|
564
|
+
...(stringValue(record.finishReason) ? { finishReason: stringValue(record.finishReason) } : {}),
|
|
565
|
+
...(stringValue(record.toolCalls) ? { toolCalls: stringValue(record.toolCalls) } : {}),
|
|
291
566
|
...(stringValue(record.platformMessageId) ? { platformMessageId: stringValue(record.platformMessageId) } : {}),
|
|
292
567
|
}];
|
|
293
568
|
}
|
|
@@ -340,20 +615,60 @@ function cloneCursor(cursor) {
|
|
|
340
615
|
openTurnsBySession: Object.fromEntries(Object.entries(cursor.openTurnsBySession).map(([key, value]) => [key, { ...value }])),
|
|
341
616
|
};
|
|
342
617
|
}
|
|
343
|
-
function
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
:
|
|
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}`);
|
|
347
661
|
return {
|
|
348
662
|
turnId,
|
|
349
663
|
runId: turnId,
|
|
350
|
-
|
|
664
|
+
mobileTurn,
|
|
665
|
+
...(mobileSessionKey ? { sessionKey: mobileSessionKey } : {}),
|
|
351
666
|
};
|
|
352
667
|
}
|
|
353
668
|
function buildUserPayload(gatewayId, row, turn, text) {
|
|
354
669
|
const event = buildTimelineEvent({
|
|
355
670
|
gatewayId,
|
|
356
|
-
sessionKey:
|
|
671
|
+
sessionKey: hermesStateDbTurnSessionKey(row.sessionId, turn),
|
|
357
672
|
row,
|
|
358
673
|
turn,
|
|
359
674
|
eventType: "turn.user.created",
|
|
@@ -373,7 +688,7 @@ function buildUserPayload(gatewayId, row, turn, text) {
|
|
|
373
688
|
function buildOutputPayload(gatewayId, row, turn, text) {
|
|
374
689
|
const completed = buildTimelineEvent({
|
|
375
690
|
gatewayId,
|
|
376
|
-
sessionKey:
|
|
691
|
+
sessionKey: hermesStateDbTurnSessionKey(row.sessionId, turn),
|
|
377
692
|
row,
|
|
378
693
|
turn,
|
|
379
694
|
eventType: "message.completed",
|
|
@@ -381,6 +696,9 @@ function buildOutputPayload(gatewayId, row, turn, text) {
|
|
|
381
696
|
messageState: "completed",
|
|
382
697
|
runState: "active",
|
|
383
698
|
content: [{ type: "text", text }],
|
|
699
|
+
...(turn.mobileTurn && row.role === "assistant"
|
|
700
|
+
? { messageId: `assistant-${turn.runId}` }
|
|
701
|
+
: {}),
|
|
384
702
|
});
|
|
385
703
|
const runCompleted = buildTimelineEvent({
|
|
386
704
|
gatewayId,
|
|
@@ -404,7 +722,11 @@ function buildOutputPayload(gatewayId, row, turn, text) {
|
|
|
404
722
|
};
|
|
405
723
|
}
|
|
406
724
|
function buildTimelineEvent(params) {
|
|
407
|
-
const messageId = params.messageId ?? hermesStateDbMessageId(
|
|
725
|
+
const messageId = params.messageId ?? hermesStateDbMessageId({
|
|
726
|
+
sessionId: params.row.sessionId,
|
|
727
|
+
rowId: params.row.id,
|
|
728
|
+
role: params.row.role,
|
|
729
|
+
});
|
|
408
730
|
return parseCanonicalTimelineEvent({
|
|
409
731
|
protocolVersion: 2,
|
|
410
732
|
eventId: `evt-${messageId}-${params.eventType}`,
|
|
@@ -431,8 +753,8 @@ function buildTimelineEvent(params) {
|
|
|
431
753
|
function hermesStateDbSessionKey(sessionId) {
|
|
432
754
|
return sessionId.startsWith("hermes:") ? sessionId : `hermes:${sessionId}`;
|
|
433
755
|
}
|
|
434
|
-
function
|
|
435
|
-
return
|
|
756
|
+
function hermesStateDbTurnSessionKey(sessionId, turn) {
|
|
757
|
+
return turn.sessionKey?.trim() || hermesStateDbSessionKey(sessionId);
|
|
436
758
|
}
|
|
437
759
|
function normalizeHermesStateDbUserText(text) {
|
|
438
760
|
return stripClawConnectMetadata(text)
|
|
@@ -460,6 +782,51 @@ function stripClawConnectMetadata(text) {
|
|
|
460
782
|
function isClawConnectMobileTurn(text) {
|
|
461
783
|
return text.includes(CLAWCONNECT_MOBILE_TURN_MARKER);
|
|
462
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
|
+
}
|
|
463
830
|
function timestampToIso(timestamp) {
|
|
464
831
|
return new Date(timestamp < 10_000_000_000 ? timestamp * 1_000 : timestamp).toISOString();
|
|
465
832
|
}
|