clawconnect-agent 0.1.31 → 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.
Files changed (46) hide show
  1. package/dist/core/relay/timeline-event-builder.js +3 -1
  2. package/dist/core/relay/timeline-event-builder.js.map +1 -1
  3. package/dist/hermes/hermes-runtime.d.ts +1 -1
  4. package/dist/hermes/hermes-runtime.js +1 -1
  5. package/dist/hermes/hermes-runtime.js.map +1 -1
  6. package/dist/hermes/relay/hermes-state-db-realtime.d.ts +30 -1
  7. package/dist/hermes/relay/hermes-state-db-realtime.js +439 -72
  8. package/dist/hermes/relay/hermes-state-db-realtime.js.map +1 -1
  9. package/dist/hermes/runtime/hermes-runtime-api-client.d.ts +14 -1
  10. package/dist/hermes/runtime/hermes-runtime-api-client.js +155 -26
  11. package/dist/hermes/runtime/hermes-runtime-api-client.js.map +1 -1
  12. package/dist/hermes/runtime/hermes-runtime-chat.d.ts +1 -0
  13. package/dist/hermes/runtime/hermes-runtime-chat.js +24 -93
  14. package/dist/hermes/runtime/hermes-runtime-chat.js.map +1 -1
  15. package/dist/hermes/runtime/hermes-runtime-command-router.js +5 -2
  16. package/dist/hermes/runtime/hermes-runtime-command-router.js.map +1 -1
  17. package/dist/hermes/runtime/hermes-runtime-history-completion.d.ts +2 -0
  18. package/dist/hermes/runtime/hermes-runtime-history-completion.js +49 -2
  19. package/dist/hermes/runtime/hermes-runtime-history-completion.js.map +1 -1
  20. package/dist/hermes/runtime/hermes-runtime-history.js +82 -9
  21. package/dist/hermes/runtime/hermes-runtime-history.js.map +1 -1
  22. package/dist/hermes/runtime/hermes-runtime-process.js +14 -2
  23. package/dist/hermes/runtime/hermes-runtime-process.js.map +1 -1
  24. package/dist/hermes/runtime/hermes-runtime-sessions.d.ts +5 -0
  25. package/dist/hermes/runtime/hermes-runtime-sessions.js +38 -6
  26. package/dist/hermes/runtime/hermes-runtime-sessions.js.map +1 -1
  27. package/dist/hermes/runtime/hermes-runtime-state-db.d.ts +14 -0
  28. package/dist/hermes/runtime/hermes-runtime-state-db.js +116 -0
  29. package/dist/hermes/runtime/hermes-runtime-state-db.js.map +1 -1
  30. package/dist/hermes/runtime/hermes-state-db-message-identity.d.ts +12 -0
  31. package/dist/hermes/runtime/hermes-state-db-message-identity.js +11 -0
  32. package/dist/hermes/runtime/hermes-state-db-message-identity.js.map +1 -0
  33. package/dist/openclaw/gateway-client.d.ts +3 -1
  34. package/dist/openclaw/gateway-client.js +16 -4
  35. package/dist/openclaw/gateway-client.js.map +1 -1
  36. package/dist/openclaw/relay/chat-history.d.ts +2 -1
  37. package/dist/openclaw/relay/chat-history.js +26 -13
  38. package/dist/openclaw/relay/chat-history.js.map +1 -1
  39. package/dist/openclaw/relay/chat-run-identity.d.ts +25 -0
  40. package/dist/openclaw/relay/chat-run-identity.js +81 -0
  41. package/dist/openclaw/relay/chat-run-identity.js.map +1 -0
  42. package/dist/openclaw/relay-manager.js +128 -73
  43. package/dist/openclaw/relay-manager.js.map +1 -1
  44. package/dist/openclaw/runtime/local-runtime.js +4 -1
  45. package/dist/openclaw/runtime/local-runtime.js.map +1 -1
  46. 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
- try:
32
- conn = connect()
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
- print(json.dumps({"ok": True, "payload": int(row["max_id"] or 0)}, ensure_ascii=False))
51
+ return int(row["max_id"] or 0)
36
52
  elif mode == "rows":
37
- after_id = int(sys.argv[3])
38
- limit = int(sys.argv[4])
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
- payload = []
59
- for row in rows:
60
- payload.append({
61
- "id": int(row["id"]),
62
- "sessionId": row["session_id"],
63
- "sessionSource": row["session_source"],
64
- "role": row["role"],
65
- "content": row["content"],
66
- "timestamp": row["timestamp"],
67
- "active": bool(row["active"]),
68
- "observed": bool(row["observed"]),
69
- "toolName": row["tool_name"],
70
- "toolCallId": row["tool_call_id"],
71
- "platformMessageId": row["platform_message_id"],
72
- })
73
- print(json.dumps({"ok": True, "payload": payload}, ensure_ascii=False))
74
- else:
75
- print(json.dumps({"ok": False, "error": "unsupported mode"}, ensure_ascii=False))
76
- except Exception as exc:
77
- print(json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False))
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
- // ClawConnect 自己发起的移动端 turn 已经通过 API/CLI streaming 推送过;
98
- // state.db 回写只用于历史,所以后续 assistant/tool 行必须跳过到下一个 user 行。
99
- const skipUntilNextUser = isClawConnectMobileTurn(row.content);
100
- const turn = buildOpenTurn(row, skipUntilNextUser);
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 (skipUntilNextUser) {
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.skipUntilNextUser) {
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
- ?? (() => queryHermesStateDbMaxMessageId({ dbPath: params.dbPath }));
380
+ ?? (() => existsSync(params.dbPath) ? ensureQueryClient().queryMaxMessageId() : Promise.resolve(0));
143
381
  const queryRows = params.queryRows
144
- ?? ((queryParams) => queryHermesStateDbRealtimeRows({
145
- dbPath: params.dbPath,
146
- afterMessageId: queryParams.afterMessageId,
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 primeCursor() {
157
- await withErrorBoundary(async () => {
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 pollOnce() {
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
- await withErrorBoundary(async () => {
171
- const rows = await queryRows({ afterMessageId: cursor.lastMessageId });
172
- if (rows.length === 0) {
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
- const result = buildHermesStateDbRealtimePayloads({
176
- gatewayId: params.gatewayId,
177
- cursor,
178
- rows,
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
- cursor = result.cursor;
181
- for (const payload of result.payloads) {
182
- params.publishPayload(payload);
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
- void primeCursor().then(() => pollOnce());
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
- if (!timer) {
198
- return;
466
+ startGeneration += 1;
467
+ if (timer) {
468
+ clearInterval(timer);
469
+ timer = undefined;
199
470
  }
200
- clearInterval(timer);
201
- timer = undefined;
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 buildOpenTurn(row, skipUntilNextUser) {
344
- const turnId = row.role === "user"
345
- ? `hermes-db-${row.sessionId}-turn-${row.id}`
346
- : `hermes-db-${row.sessionId}-orphan-turn-${row.id}`;
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
- skipUntilNextUser,
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: hermesStateDbSessionKey(row.sessionId),
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: hermesStateDbSessionKey(row.sessionId),
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(params.row);
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 hermesStateDbMessageId(row) {
435
- return `hermes-db-${row.sessionId}-message-${row.id}-${row.role}`;
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
  }