clawconnect-agent 0.1.11 → 0.1.13

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 (102) hide show
  1. package/README.md +16 -0
  2. package/README.zh-CN.md +16 -0
  3. package/dist/commands/install.js +3 -2
  4. package/dist/commands/install.js.map +1 -1
  5. package/dist/commands/pair.js +2 -1
  6. package/dist/commands/pair.js.map +1 -1
  7. package/dist/commands/profile-hints.d.ts +7 -0
  8. package/dist/commands/profile-hints.js +71 -0
  9. package/dist/commands/profile-hints.js.map +1 -0
  10. package/dist/commands/send-file.js +25 -5
  11. package/dist/commands/send-file.js.map +1 -1
  12. package/dist/core/relay/timeline-event-builder.d.ts +5 -1
  13. package/dist/core/relay/timeline-event-builder.js +4 -0
  14. package/dist/core/relay/timeline-event-builder.js.map +1 -1
  15. package/dist/core/relay/timeline-event-log.d.ts +3 -0
  16. package/dist/core/relay/timeline-event-log.js +4 -0
  17. package/dist/core/relay/timeline-event-log.js.map +1 -1
  18. package/dist/hermes/hermes-relay-manager.d.ts +27 -24
  19. package/dist/hermes/hermes-relay-manager.js +126 -343
  20. package/dist/hermes/hermes-relay-manager.js.map +1 -1
  21. package/dist/hermes/hermes-runtime-test-support.d.ts +27 -0
  22. package/dist/hermes/hermes-runtime-test-support.js +703 -0
  23. package/dist/hermes/hermes-runtime-test-support.js.map +1 -0
  24. package/dist/hermes/hermes-runtime.d.ts +1 -2
  25. package/dist/hermes/hermes-runtime.js +1 -2
  26. package/dist/hermes/hermes-runtime.js.map +1 -1
  27. package/dist/hermes/relay/active-hermes-chat-runs.d.ts +12 -0
  28. package/dist/hermes/relay/active-hermes-chat-runs.js +42 -0
  29. package/dist/hermes/relay/active-hermes-chat-runs.js.map +1 -0
  30. package/dist/hermes/relay/hermes-slash-command-catalog.d.ts +22 -0
  31. package/dist/hermes/relay/hermes-slash-command-catalog.js +248 -0
  32. package/dist/hermes/relay/hermes-slash-command-catalog.js.map +1 -0
  33. package/dist/hermes/relay/hermes-state-db-realtime.d.ts +69 -0
  34. package/dist/hermes/relay/hermes-state-db-realtime.js +444 -0
  35. package/dist/hermes/relay/hermes-state-db-realtime.js.map +1 -0
  36. package/dist/hermes/runtime/hermes-runtime-api-client.d.ts +15 -0
  37. package/dist/hermes/runtime/hermes-runtime-api-client.js +510 -0
  38. package/dist/hermes/runtime/hermes-runtime-api-client.js.map +1 -0
  39. package/dist/hermes/runtime/hermes-runtime-chat.d.ts +4 -14
  40. package/dist/hermes/runtime/hermes-runtime-chat.js +205 -436
  41. package/dist/hermes/runtime/hermes-runtime-chat.js.map +1 -1
  42. package/dist/hermes/runtime/hermes-runtime-command-utils.d.ts +1 -0
  43. package/dist/hermes/runtime/hermes-runtime-command-utils.js +10 -1
  44. package/dist/hermes/runtime/hermes-runtime-command-utils.js.map +1 -1
  45. package/dist/hermes/runtime/hermes-runtime-history-completion.d.ts +12 -0
  46. package/dist/hermes/runtime/hermes-runtime-history-completion.js +162 -0
  47. package/dist/hermes/runtime/hermes-runtime-history-completion.js.map +1 -0
  48. package/dist/hermes/runtime/hermes-runtime-history.js +85 -22
  49. package/dist/hermes/runtime/hermes-runtime-history.js.map +1 -1
  50. package/dist/hermes/runtime/hermes-runtime-preloaded-skills.d.ts +11 -0
  51. package/dist/hermes/runtime/hermes-runtime-preloaded-skills.js +67 -0
  52. package/dist/hermes/runtime/hermes-runtime-preloaded-skills.js.map +1 -0
  53. package/dist/hermes/runtime/hermes-runtime-process.d.ts +3 -1
  54. package/dist/hermes/runtime/hermes-runtime-process.js +35 -6
  55. package/dist/hermes/runtime/hermes-runtime-process.js.map +1 -1
  56. package/dist/hermes/runtime/hermes-runtime-sessions.js +28 -3
  57. package/dist/hermes/runtime/hermes-runtime-sessions.js.map +1 -1
  58. package/dist/hermes/runtime/hermes-runtime-slash-command.d.ts +8 -0
  59. package/dist/hermes/runtime/hermes-runtime-slash-command.js +144 -0
  60. package/dist/hermes/runtime/hermes-runtime-slash-command.js.map +1 -0
  61. package/dist/hermes/runtime/hermes-runtime-state-db.d.ts +4 -0
  62. package/dist/hermes/runtime/hermes-runtime-state-db.js +193 -0
  63. package/dist/hermes/runtime/hermes-runtime-state-db.js.map +1 -0
  64. package/dist/hermes/runtime/hermes-runtime-tool-log-watcher.d.ts +8 -0
  65. package/dist/hermes/runtime/hermes-runtime-tool-log-watcher.js +121 -0
  66. package/dist/hermes/runtime/hermes-runtime-tool-log-watcher.js.map +1 -0
  67. package/dist/hermes/runtime/hermes-runtime-usage.d.ts +2 -0
  68. package/dist/hermes/runtime/hermes-runtime-usage.js +59 -5
  69. package/dist/hermes/runtime/hermes-runtime-usage.js.map +1 -1
  70. package/dist/i18n/index.js +4 -0
  71. package/dist/i18n/index.js.map +1 -1
  72. package/dist/index.js +22 -3
  73. package/dist/index.js.map +1 -1
  74. package/dist/openclaw/handlers/local-handlers.js +66 -20
  75. package/dist/openclaw/handlers/local-handlers.js.map +1 -1
  76. package/dist/openclaw/relay/chat-send-dedupe-coordinator.d.ts +20 -0
  77. package/dist/openclaw/relay/chat-send-dedupe-coordinator.js +161 -0
  78. package/dist/openclaw/relay/chat-send-dedupe-coordinator.js.map +1 -0
  79. package/dist/openclaw/relay/relay-manager-hello.d.ts +6 -0
  80. package/dist/openclaw/relay/relay-manager-hello.js +11 -0
  81. package/dist/openclaw/relay/relay-manager-hello.js.map +1 -0
  82. package/dist/openclaw/relay/relay-manager-history-timing.d.ts +5 -0
  83. package/dist/openclaw/relay/relay-manager-history-timing.js +6 -0
  84. package/dist/openclaw/relay/relay-manager-history-timing.js.map +1 -0
  85. package/dist/openclaw/relay/relay-manager-office-events.d.ts +2 -0
  86. package/dist/openclaw/relay/relay-manager-office-events.js +9 -0
  87. package/dist/openclaw/relay/relay-manager-office-events.js.map +1 -0
  88. package/dist/openclaw/relay/relay-manager-payload-helpers.d.ts +18 -0
  89. package/dist/openclaw/relay/relay-manager-payload-helpers.js +213 -0
  90. package/dist/openclaw/relay/relay-manager-payload-helpers.js.map +1 -0
  91. package/dist/openclaw/relay/relay-manager-protocol.d.ts +49 -0
  92. package/dist/openclaw/relay/relay-manager-protocol.js +2 -0
  93. package/dist/openclaw/relay/relay-manager-protocol.js.map +1 -0
  94. package/dist/openclaw/relay-manager.d.ts +3 -27
  95. package/dist/openclaw/relay-manager.js +19 -402
  96. package/dist/openclaw/relay-manager.js.map +1 -1
  97. package/dist/openclaw/session-store.js +42 -1
  98. package/dist/openclaw/session-store.js.map +1 -1
  99. package/package.json +1 -1
  100. package/dist/hermes/runtime/hermes-runtime-artifacts.d.ts +0 -3
  101. package/dist/hermes/runtime/hermes-runtime-artifacts.js +0 -75
  102. package/dist/hermes/runtime/hermes-runtime-artifacts.js.map +0 -1
@@ -1,94 +1,35 @@
1
1
  import { randomUUID } from "crypto";
2
- import { readFileSync, statSync } from "fs";
3
2
  import { mkdir, writeFile } from "fs/promises";
4
3
  import { join } from "path";
5
4
  import { spawn } from "child_process";
6
5
  import { buildMobileAssistantDeltaPayload, } from "../../core/relay/mobile-chat-run-bridge.js";
7
6
  import { buildToolInvocationUpdatedEvent, } from "../../core/relay/timeline-event-builder.js";
8
7
  import { forgetHermesSession, getMappedHermesSessionId, rememberHermesSession, } from "../hermes-session-store.js";
9
- import { extractDeliverablePaths } from "./hermes-runtime-artifacts.js";
10
- import { runHermesSessionExport } from "./hermes-runtime-sessions.js";
11
- import { CHAT_TIMEOUT_MS, CLAWCONNECT_MOBILE_BRIDGE_HINT, HERMES_AGENT_LOG_FILE, HERMES_INBOX_DIR, SUBPROCESS_ENV, resolveHermesBin, isHermesCommandDeniedTimeoutLine, isHermesMissingSessionError, runHermes, runHermesPython, sanitizeHermesChatOutput, stripAnsi, stripHermesSessionResumeNotices, } from "./hermes-runtime-process.js";
12
- import { collectHermesUsageSnapshot, listHermesSessions, readHermesStatusSnapshot } from "./hermes-runtime-usage.js";
13
- import { compactStringArray, sanitizeFileName, toRecord } from "./hermes-runtime-values.js";
8
+ import { CHAT_TIMEOUT_MS, CLAWCONNECT_MOBILE_BRIDGE_HINT, HERMES_INBOX_DIR, SUBPROCESS_ENV, resolveHermesBin, isHermesCommandDeniedTimeoutLine, isHermesMissingSessionError, runHermes, sanitizeHermesChatOutput, stripAnsi, stripHermesSessionResumeNotices, } from "./hermes-runtime-process.js";
9
+ import { collectHermesUsageSnapshot, listHermesSessions, readCachedHermesStatusSnapshot, readHermesStatusSnapshotAsync, } from "./hermes-runtime-usage.js";
10
+ import { compactStringArray, sanitizeFileName } from "./hermes-runtime-values.js";
11
+ import { detectHermesHistoryCompletion, selectHermesSessionForCompletedChat, } from "./hermes-runtime-history-completion.js";
12
+ import { tryRunHermesApiChat } from "./hermes-runtime-api-client.js";
13
+ import { resolveHermesPreloadedSkillContext } from "./hermes-runtime-preloaded-skills.js";
14
+ import { createHermesToolLogWatcher, hermesToolState, } from "./hermes-runtime-tool-log-watcher.js";
15
+ import { isHermesSlashCommandMessage, runHermesSlashCommand, } from "./hermes-runtime-slash-command.js";
16
+ export { selectHermesSessionForCompletedChat } from "./hermes-runtime-history-completion.js";
17
+ export { parseHermesToolLogLine } from "./hermes-runtime-tool-log-watcher.js";
18
+ export { isHermesSlashCommandMessage } from "./hermes-runtime-slash-command.js";
14
19
  const HERMES_ASSISTANT_DELTA_FLUSH_MS = 120;
15
20
  const HERMES_ASSISTANT_DELTA_MAX_BYTES = 4096;
16
21
  const HERMES_HISTORY_COMPLETION_GRACE_MS = 2_000;
17
22
  const HERMES_HISTORY_COMPLETION_POLL_MS = 1_000;
23
+ const HERMES_API_EMPTY_OUTPUT_HISTORY_COMPLETION_TIMEOUT_MS = 12_000;
24
+ const HERMES_API_EMPTY_OUTPUT_HISTORY_COMPLETION_POLL_MS = 500;
25
+ const HERMES_RUNTIME_CONTEXT_CACHE_MAX_AGE_MS = 5 * 60_000;
18
26
  const HERMES_COMMAND_DENIED_TIMEOUT_MESSAGE = "Timeout – denying command";
19
27
  const hermesChatQueues = new Map();
20
- const HERMES_SLASH_COMMAND_SCRIPT = String.raw `
21
- import contextlib
22
- import io
23
- import json
24
- import os
25
- import sys
26
-
27
- from rich.console import Console
28
-
29
- import cli as cli_mod
30
- from cli import HermesCLI
31
-
32
- command = os.environ.get("CLAWCONNECT_HERMES_SLASH_COMMAND", "").strip()
33
- resume = os.environ.get("CLAWCONNECT_HERMES_SLASH_RESUME", "").strip() or None
34
- if command and not command.startswith("/"):
35
- command = "/" + command
36
-
37
- with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
38
- cli = HermesCLI(model=None, compact=True, resume=resume, verbose=False)
39
-
40
- buf = io.StringIO()
41
- cli.console = Console(file=buf, force_terminal=True, width=120)
42
-
43
- def approve_once(prompt=""):
44
- if prompt:
45
- print(prompt, end="")
46
- return "1"
47
-
48
- def approve_once_modal(*args, **kwargs):
49
- return "once"
50
-
51
- try:
52
- cli._prompt_text_input = approve_once
53
- cli._prompt_text_input_modal = approve_once_modal
54
- except Exception:
55
- pass
56
-
57
- old_cprint = getattr(cli_mod, "_cprint", None)
58
- if old_cprint is not None:
59
- cli_mod._cprint = lambda text: print(text)
60
-
61
- try:
62
- with contextlib.redirect_stdout(buf), contextlib.redirect_stderr(buf):
63
- keep_going = cli.process_command(command)
64
- pending_inputs = []
65
- pending_queue = getattr(cli, "_pending_input", None)
66
- if pending_queue is not None:
67
- while True:
68
- try:
69
- pending_inputs.append(pending_queue.get_nowait())
70
- except Exception:
71
- break
72
- payload = {
73
- "ok": True,
74
- "output": buf.getvalue().rstrip(),
75
- "sessionId": getattr(cli, "session_id", None),
76
- "keepGoing": bool(keep_going),
77
- "pendingInputs": pending_inputs,
78
- }
79
- except Exception as exc:
80
- payload = {
81
- "ok": False,
82
- "output": buf.getvalue().rstrip(),
83
- "error": str(exc),
84
- "sessionId": getattr(cli, "session_id", None),
85
- }
86
- finally:
87
- if old_cprint is not None:
88
- cli_mod._cprint = old_cprint
89
-
90
- sys.stdout.write(json.dumps(payload, ensure_ascii=False))
91
- `;
28
+ const EMPTY_PRELOADED_SKILL_CONTEXT = {
29
+ cliArgs: [],
30
+ requiredToolsets: [],
31
+ skillNames: [],
32
+ };
92
33
  export async function runHermesChat(params, context = {}) {
93
34
  const record = params && typeof params === "object" && !Array.isArray(params)
94
35
  ? params
@@ -98,19 +39,33 @@ export async function runHermesChat(params, context = {}) {
98
39
  ? record.sessionKey.trim()
99
40
  : "main";
100
41
  if (isHermesSlashCommandMessage(rawMessage)) {
101
- return await runSerializedHermesChat(sessionKey, () => (runHermesSlashCommand({ message: rawMessage, sessionKey, hermesSessionId: record.hermesSessionId })));
42
+ return await runSerializedHermesChat(sessionKey, () => (runHermesSlashCommand({
43
+ message: rawMessage,
44
+ sessionKey,
45
+ hermesSessionId: record.hermesSessionId,
46
+ runQueuedChat: (queuedMessage, hermesSessionId) => runHermesChat({
47
+ message: queuedMessage,
48
+ sessionKey,
49
+ ...(hermesSessionId ? { hermesSessionId } : {}),
50
+ }),
51
+ })));
102
52
  }
103
- const message = await prepareHermesMessage(rawMessage, record.attachments, sessionKey);
104
- if (!message.trim()) {
53
+ const sourceRunId = typeof context.requestId === "string" && context.requestId.trim().length > 0
54
+ ? context.requestId.trim()
55
+ : undefined;
56
+ const preparationPlan = planHermesChatPreparation(rawMessage);
57
+ const preparedMessage = await prepareHermesMessage(rawMessage, record.attachments, sessionKey, sourceRunId, preparationPlan);
58
+ if (!preparedMessage.cliMessage.trim()) {
105
59
  throw new Error("message_required");
106
60
  }
107
61
  return await runSerializedHermesChat(sessionKey, async () => {
108
62
  return await runHermesChatPrepared({
109
63
  rawMessage,
110
- message,
64
+ preparedMessage,
111
65
  sessionKey,
112
66
  hermesSessionId: record.hermesSessionId,
113
67
  context,
68
+ preparationPlan,
114
69
  });
115
70
  });
116
71
  }
@@ -135,14 +90,64 @@ async function runHermesChatPrepared(params) {
135
90
  : undefined;
136
91
  const mappedResume = explicitResume ? undefined : await getMappedHermesSessionId(params.sessionKey);
137
92
  let resume = explicitResume ?? mappedResume;
93
+ const preloadedSkillContext = params.preparationPlan.preloadFileTransferSkill
94
+ ? await resolveHermesPreloadedSkillContext({ forceFileTransfer: true })
95
+ : EMPTY_PRELOADED_SKILL_CONTEXT;
96
+ try {
97
+ const apiChat = await tryRunHermesApiChat({
98
+ message: params.preparedMessage.apiMessage,
99
+ instructions: params.preparedMessage.apiInstructions,
100
+ sessionKey: params.sessionKey,
101
+ resume,
102
+ preloadedSkillNames: preloadedSkillContext.skillNames,
103
+ requiredToolsets: preloadedSkillContext.requiredToolsets,
104
+ context: params.context,
105
+ });
106
+ if (apiChat) {
107
+ const recoveredOutput = await recoverEmptyHermesApiOutputFromHistory({
108
+ output: apiChat.output,
109
+ hermesSessionId: apiChat.hermesSessionId,
110
+ sessionKey: params.sessionKey,
111
+ userMessage: params.rawMessage,
112
+ abortSignal: params.context.abortSignal,
113
+ });
114
+ return recoveredOutput ? { ...apiChat, output: recoveredOutput } : apiChat;
115
+ }
116
+ }
117
+ catch (error) {
118
+ if (!mappedResume || !isHermesMissingSessionError(error)) {
119
+ throw error;
120
+ }
121
+ await forgetHermesSession(params.sessionKey, mappedResume);
122
+ resume = undefined;
123
+ const retryApiChat = await tryRunHermesApiChat({
124
+ message: params.preparedMessage.apiMessage,
125
+ instructions: params.preparedMessage.apiInstructions,
126
+ sessionKey: params.sessionKey,
127
+ preloadedSkillNames: preloadedSkillContext.skillNames,
128
+ requiredToolsets: preloadedSkillContext.requiredToolsets,
129
+ context: params.context,
130
+ });
131
+ if (retryApiChat) {
132
+ const recoveredOutput = await recoverEmptyHermesApiOutputFromHistory({
133
+ output: retryApiChat.output,
134
+ hermesSessionId: retryApiChat.hermesSessionId,
135
+ sessionKey: params.sessionKey,
136
+ userMessage: params.rawMessage,
137
+ abortSignal: params.context.abortSignal,
138
+ });
139
+ return recoveredOutput ? { ...retryApiChat, output: recoveredOutput } : retryApiChat;
140
+ }
141
+ }
138
142
  const beforeSessions = await listHermesSessions();
139
143
  let rawOutput;
140
144
  try {
141
145
  rawOutput = await runHermesChatOnce({
142
- message: params.message,
146
+ message: params.preparedMessage.cliMessage,
143
147
  sessionKey: params.sessionKey,
144
148
  resume,
145
149
  context: params.context,
150
+ preloadedSkillArgs: preloadedSkillContext.cliArgs,
146
151
  historyCompletion: () => detectHermesHistoryCompletion({
147
152
  beforeSessions,
148
153
  resume,
@@ -158,9 +163,10 @@ async function runHermesChatPrepared(params) {
158
163
  await forgetHermesSession(params.sessionKey, mappedResume);
159
164
  resume = undefined;
160
165
  rawOutput = await runHermesChatOnce({
161
- message: params.message,
166
+ message: params.preparedMessage.cliMessage,
162
167
  sessionKey: params.sessionKey,
163
168
  context: params.context,
169
+ preloadedSkillArgs: preloadedSkillContext.cliArgs,
164
170
  historyCompletion: () => detectHermesHistoryCompletion({
165
171
  beforeSessions,
166
172
  sessionKey: params.sessionKey,
@@ -180,257 +186,72 @@ async function runHermesChatPrepared(params) {
180
186
  }
181
187
  const usage = mappedSession?.hermesSessionId
182
188
  ? await collectHermesUsageSnapshot(mappedSession.hermesSessionId)
183
- : readHermesStatusSnapshot();
189
+ : await readHermesStatusSnapshotAsync();
184
190
  return {
185
191
  output,
186
192
  sessionKey: params.sessionKey,
187
- artifactPaths: extractDeliverablePaths(output, { userMessage: params.rawMessage }),
193
+ artifactPaths: [],
188
194
  usage,
189
195
  };
190
196
  }
191
- async function runHermesChatOnce(params) {
192
- const args = ["chat", "--query", params.message, "--quiet", "--source", "pocketclaw", "--yolo"];
193
- if (params.resume) {
194
- args.push("--resume", params.resume);
195
- }
196
- return params.context.publishEvent
197
- ? await runHermesChatStreaming(args, params.sessionKey, params.context, params.historyCompletion)
198
- : runHermes(args, CHAT_TIMEOUT_MS);
199
- }
200
- async function detectHermesHistoryCompletion(params) {
201
- const sessions = await listHermesSessions();
202
- const mappedSession = selectHermesSessionForCompletedChat(sessions, {
203
- beforeSessions: params.beforeSessions,
204
- resume: params.resume,
205
- userMessage: params.userMessage,
206
- });
207
- if (!mappedSession?.hermesSessionId) {
208
- return undefined;
209
- }
210
- const exportResult = await runHermesSessionExport({
211
- sessionKey: params.sessionKey,
212
- hermesSessionId: mappedSession.hermesSessionId,
213
- output: "-",
214
- });
215
- if (!exportResult.ok) {
197
+ async function recoverEmptyHermesApiOutputFromHistory(params) {
198
+ if (sanitizeHermesChatOutput(params.output).trim()) {
216
199
  return undefined;
217
200
  }
218
- return latestAssistantReplyFromHermesExport(exportResult.payload, params.userMessage);
219
- }
220
- function latestAssistantReplyFromHermesExport(payload, userMessage) {
221
- const output = toRecord(payload).output;
222
- const parsed = parseHermesChatExportOutput(output);
223
- const record = toRecord(parsed);
224
- const rawMessages = Array.isArray(record.messages)
225
- ? record.messages
226
- : Array.isArray(record.items)
227
- ? record.items
228
- : Array.isArray(parsed)
229
- ? parsed
230
- : [];
231
- if (rawMessages.length === 0) {
232
- return undefined;
233
- }
234
- const normalizedUser = normalizeSessionSelectionText(userMessage);
235
- if (!normalizedUser) {
201
+ if (!params.hermesSessionId?.trim()) {
236
202
  return undefined;
237
203
  }
238
- let latestUserIndex = -1;
239
- let latestUserMatchesCurrentRequest = false;
240
- for (let index = rawMessages.length - 1; index >= 0; index -= 1) {
241
- const message = toRecord(rawMessages[index]);
242
- if (normalizeHistoryRoleValue(message.role) !== "user") {
243
- continue;
204
+ const deadline = Date.now() + HERMES_API_EMPTY_OUTPUT_HISTORY_COMPLETION_TIMEOUT_MS;
205
+ while (Date.now() <= deadline) {
206
+ if (params.abortSignal?.aborted) {
207
+ throw new Error("hermes_chat_aborted");
244
208
  }
245
- const text = normalizeSessionSelectionText(extractHermesHistoryText(message));
246
- latestUserIndex = index;
247
- latestUserMatchesCurrentRequest = text.length > 0
248
- && (text.includes(normalizedUser) || normalizedUser.includes(text));
249
- break;
250
- }
251
- if (latestUserIndex < 0 || !latestUserMatchesCurrentRequest) {
252
- return undefined;
253
- }
254
- for (let index = rawMessages.length - 1; index >= latestUserIndex + 1; index -= 1) {
255
- const message = toRecord(rawMessages[index]);
256
- if (normalizeHistoryRoleValue(message.role) !== "assistant") {
257
- continue;
209
+ const detectedOutput = await detectHermesHistoryCompletion({
210
+ beforeSessions: [],
211
+ resume: params.hermesSessionId,
212
+ sessionKey: params.sessionKey,
213
+ userMessage: params.userMessage,
214
+ });
215
+ if (detectedOutput) {
216
+ return detectedOutput;
258
217
  }
259
- const text = sanitizeHermesChatOutput(extractHermesHistoryText(message)).trim();
260
- if (text) {
261
- return text;
218
+ const remainingMs = deadline - Date.now();
219
+ if (remainingMs <= 0) {
220
+ break;
262
221
  }
222
+ await sleep(Math.min(HERMES_API_EMPTY_OUTPUT_HISTORY_COMPLETION_POLL_MS, remainingMs));
263
223
  }
264
224
  return undefined;
265
225
  }
266
- function parseHermesChatExportOutput(value) {
267
- if (typeof value !== "string") {
268
- return value;
269
- }
270
- const trimmed = value.trim();
271
- if (!trimmed) {
272
- return {};
273
- }
274
- try {
275
- return JSON.parse(trimmed);
276
- }
277
- catch {
278
- const firstObjectBrace = trimmed.indexOf("{");
279
- const lastObjectBrace = trimmed.lastIndexOf("}");
280
- if (firstObjectBrace >= 0 && lastObjectBrace > firstObjectBrace) {
281
- try {
282
- return JSON.parse(trimmed.slice(firstObjectBrace, lastObjectBrace + 1));
283
- }
284
- catch {
285
- return {};
286
- }
287
- }
288
- return {};
289
- }
290
- }
291
- function normalizeHistoryRoleValue(value) {
292
- return typeof value === "string" ? value.trim().toLowerCase().replace("_", "") : "";
293
- }
294
- function extractHermesHistoryText(record) {
295
- const content = record.content;
296
- if (typeof content === "string") {
297
- return content;
298
- }
299
- if (Array.isArray(content)) {
300
- return content
301
- .flatMap((block) => {
302
- const blockRecord = toRecord(block);
303
- const type = typeof blockRecord.type === "string" ? blockRecord.type.trim().toLowerCase() : "";
304
- if (type && type !== "text" && type !== "output_text" && type !== "input_text") {
305
- return [];
306
- }
307
- return typeof blockRecord.text === "string" ? [blockRecord.text] : [];
308
- })
309
- .filter((text) => text.trim().length > 0)
310
- .join("\n\n");
311
- }
312
- for (const key of ["text", "message", "output"]) {
313
- const value = record[key];
314
- if (typeof value === "string" && value.trim()) {
315
- return value;
316
- }
317
- }
318
- return "";
226
+ async function sleep(ms) {
227
+ await new Promise((resolve) => setTimeout(resolve, ms));
319
228
  }
320
- export function isHermesSlashCommandMessage(message) {
321
- return /^\/[A-Za-z0-9][\w-]*(?:\s|$)/.test(message.trim());
322
- }
323
- export function selectHermesSessionForCompletedChat(sessions, options = {}) {
324
- const resume = options.resume?.trim();
325
- if (resume) {
326
- return sessions.find((session) => session.hermesSessionId === resume);
327
- }
328
- const beforeIds = new Set((options.beforeSessions ?? []).map((session) => session.hermesSessionId));
329
- const newSessions = sessions.filter((session) => !beforeIds.has(session.hermesSessionId));
330
- if (newSessions.length === 0) {
331
- return sessions[0];
332
- }
333
- const normalizedUserMessage = normalizeSessionSelectionText(options.userMessage);
334
- if (normalizedUserMessage) {
335
- const matched = newSessions.find((session) => {
336
- const haystack = normalizeSessionSelectionText([
337
- session.displayName,
338
- session.derivedTitle,
339
- session.label,
340
- ].filter(Boolean).join(" "));
341
- return haystack.length > 0
342
- && (haystack.includes(normalizedUserMessage) || normalizedUserMessage.includes(haystack));
343
- });
344
- if (matched) {
345
- return matched;
346
- }
347
- }
348
- return newSessions[0];
349
- }
350
- function normalizeSessionSelectionText(value) {
351
- return value
352
- ?.replace(/\[Hermes runtime context][\s\S]*$/i, "")
353
- .replace(/\s+/g, " ")
354
- .trim()
355
- .slice(0, 80)
356
- .toLowerCase() ?? "";
357
- }
358
- export async function runHermesSlashCommand(params) {
359
- const command = params.message.trim();
360
- const resume = typeof params.hermesSessionId === "string" && params.hermesSessionId.trim().length > 0
361
- ? params.hermesSessionId.trim()
362
- : await getMappedHermesSessionId(params.sessionKey);
363
- const raw = runHermesPython(HERMES_SLASH_COMMAND_SCRIPT, {
364
- CLAWCONNECT_HERMES_SLASH_COMMAND: command,
365
- CLAWCONNECT_HERMES_SLASH_RESUME: resume ?? "",
366
- });
367
- const payload = parseHermesSlashCommandPayload(raw);
368
- const output = sanitizeHermesChatOutput(payload.output ?? "").trim();
369
- if (!payload.ok) {
370
- const message = [output, payload.error].filter(Boolean).join("\n").trim() || "hermes_slash_command_failed";
371
- throw new Error(message);
372
- }
373
- const sessionId = typeof payload.sessionId === "string" && payload.sessionId.trim().length > 0
374
- ? payload.sessionId.trim()
375
- : undefined;
376
- if (sessionId) {
377
- await rememberHermesSession(params.sessionKey, {
378
- sessionKey: params.sessionKey,
379
- hermesSessionId: sessionId,
380
- displayName: sessionId,
381
- label: command,
382
- lastActivityAt: new Date().toISOString(),
383
- kind: "hermes",
384
- });
385
- }
386
- if (/^\/(?:new|reset)\b/i.test(command) && resume && sessionId && sessionId !== resume) {
387
- await forgetHermesSession(resume, resume);
388
- }
389
- if (payload.pendingInputs && payload.pendingInputs.length > 0) {
390
- const queuedMessage = payload.pendingInputs.join("\n\n").trim();
391
- if (queuedMessage) {
392
- const queued = await runHermesChat({
393
- message: queuedMessage,
394
- sessionKey: params.sessionKey,
395
- ...(sessionId ? { hermesSessionId: sessionId } : {}),
396
- });
397
- return {
398
- ...queued,
399
- output: [output, queued.output].filter((part) => part && part !== "(no output)").join("\n\n") || queued.output,
400
- };
401
- }
402
- }
403
- const usage = await collectHermesUsageSnapshot(sessionId);
404
- return {
405
- output: output || "(no output)",
406
- sessionKey: params.sessionKey,
407
- artifactPaths: [],
408
- usage,
409
- };
410
- }
411
- function parseHermesSlashCommandPayload(raw) {
412
- const parsed = JSON.parse(raw);
413
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
414
- throw new Error("invalid_hermes_slash_command_payload");
229
+ async function runHermesChatOnce(params) {
230
+ const args = [
231
+ "chat",
232
+ "--query",
233
+ params.message,
234
+ "--quiet",
235
+ "--source",
236
+ "pocketclaw",
237
+ ...(params.preloadedSkillArgs ?? []),
238
+ "--yolo",
239
+ ];
240
+ if (params.resume) {
241
+ args.push("--resume", params.resume);
415
242
  }
416
- const record = parsed;
417
- return {
418
- ok: record.ok === true,
419
- output: typeof record.output === "string" ? record.output : undefined,
420
- error: typeof record.error === "string" ? record.error : undefined,
421
- sessionId: typeof record.sessionId === "string" ? record.sessionId : undefined,
422
- pendingInputs: Array.isArray(record.pendingInputs)
423
- ? record.pendingInputs.filter((value) => typeof value === "string" && value.trim().length > 0)
424
- : undefined,
425
- };
243
+ const runId = params.context.requestId ?? `hermes-${Date.now()}`;
244
+ const env = hermesChatSubprocessEnv(runId, params.sessionKey);
245
+ return params.context.publishEvent
246
+ ? await runHermesChatStreaming(args, params.sessionKey, params.context, runId, env, params.historyCompletion)
247
+ : runHermes(args, CHAT_TIMEOUT_MS, env);
426
248
  }
427
- async function runHermesChatStreaming(args, sessionKey, context, historyCompletion) {
249
+ async function runHermesChatStreaming(args, sessionKey, context, runId, env, historyCompletion) {
428
250
  const child = spawn(resolveHermesBin(), args, {
429
- env: SUBPROCESS_ENV,
251
+ env,
430
252
  stdio: ["ignore", "pipe", "pipe"],
431
253
  windowsHide: true,
432
254
  });
433
- const runId = context.requestId ?? `hermes-${Date.now()}`;
434
255
  let output = "";
435
256
  let stderr = "";
436
257
  let seq = 0;
@@ -552,7 +373,8 @@ async function runHermesChatStreaming(args, sessionKey, context, historyCompleti
552
373
  sessionKey,
553
374
  seq: seq += 1,
554
375
  timestampMs,
555
- delta,
376
+ // canonical message.part.delta 是同一 part 的绝对状态;Hermes CLI stdout 这里 flush 的只是增量片段。
377
+ delta: output,
556
378
  }),
557
379
  });
558
380
  }
@@ -797,125 +619,27 @@ export function buildHermesAssistantDeltaPayload(params) {
797
619
  includeTimelineEvents: true,
798
620
  });
799
621
  }
800
- function hermesToolState(event) {
801
- if (event.phase === "completed") {
802
- return "success";
803
- }
804
- if (event.phase === "failed") {
805
- return "failed";
806
- }
807
- return "streaming_output";
808
- }
809
- function createHermesToolLogWatcher(onEvent) {
810
- let offset = 0;
811
- let timer;
812
- try {
813
- offset = statSync(HERMES_AGENT_LOG_FILE).size;
814
- }
815
- catch {
816
- offset = 0;
817
- }
818
- const poll = () => {
819
- let content = "";
820
- try {
821
- const bytes = readFileSync(HERMES_AGENT_LOG_FILE);
822
- if (bytes.length < offset) {
823
- offset = 0;
824
- }
825
- if (bytes.length === offset) {
826
- return;
827
- }
828
- content = bytes.subarray(offset).toString("utf8");
829
- offset = bytes.length;
830
- }
831
- catch {
832
- return;
833
- }
834
- for (const line of content.split(/\r?\n/)) {
835
- const event = parseHermesToolLogLine(line);
836
- if (event) {
837
- onEvent(event);
838
- }
839
- }
840
- };
622
+ function hermesChatSubprocessEnv(runId, sessionKey) {
841
623
  return {
842
- start: () => {
843
- if (timer) {
844
- return;
845
- }
846
- timer = setInterval(poll, 250);
847
- timer.unref?.();
848
- },
849
- stop: () => {
850
- poll();
851
- if (timer) {
852
- clearInterval(timer);
853
- timer = undefined;
854
- }
855
- },
624
+ ...SUBPROCESS_ENV,
625
+ CLAWCONNECT_SOURCE_RUN_ID: runId,
626
+ CLAWCONNECT_SESSION_KEY: sessionKey,
627
+ CLAWCONNECT_CHAT_SESSION_KEY: sessionKey,
856
628
  };
857
629
  }
858
- export function parseHermesToolLogLine(line) {
859
- const clean = stripAnsi(line).trim();
860
- if (!clean) {
861
- return null;
862
- }
863
- const executor = clean.match(/\bagent\.tool_executor:\s*tool\s+([A-Za-z0-9_.-]+)\s+(.+)$/i);
864
- if (executor) {
865
- const toolName = normalizeHermesToolName(executor[1] ?? "tool");
866
- const detail = (executor[2] ?? "").trim();
867
- if (/\b(?:running|started|executing|start)\b/i.test(detail)) {
868
- return {
869
- toolName,
870
- phase: "streaming",
871
- text: `${toolName} ${detail}`.trim(),
872
- isError: false,
873
- };
874
- }
875
- const failed = /\b(?:failed|error|errored|denied|aborted)\b/i.test(detail);
876
- return {
877
- toolName,
878
- phase: failed ? "failed" : "completed",
879
- text: `${toolName} ${detail}`.trim(),
880
- isError: failed,
881
- };
882
- }
883
- const toolLogger = clean.match(/\btools\.([A-Za-z0-9_.-]+):\s*(.+)$/i);
884
- if (!toolLogger) {
885
- return null;
886
- }
887
- const loggerName = toolLogger[1] ?? "tool";
888
- if (!/(?:_tool|_tools)$/i.test(loggerName)) {
889
- return null;
890
- }
891
- let toolName = normalizeHermesToolName(loggerName);
892
- let detail = (toolLogger[2] ?? "").trim();
893
- if (/\b(?:Shutting down \d+ remaining sandbox(?:\(es\))?|Manually cleaned up environment|Cleaned \d+ environments?)\b/i.test(detail)) {
894
- return null;
895
- }
896
- const nestedTool = detail.match(/^([A-Za-z0-9_.-]+):\s*(.+)$/);
897
- if (nestedTool) {
898
- toolName = normalizeHermesToolName(nestedTool[1] ?? toolName);
899
- detail = (nestedTool[2] ?? "").trim();
900
- }
901
- if (!detail) {
902
- return null;
630
+ const CLAWCONNECT_MOBILE_TURN_INSTRUCTION = "Use [ClawConnect mobile turn] metadata only for ClawConnect file-transfer attribution and message identity. Do not mention it in the answer.";
631
+ function buildClawConnectMobileTurnMetadata(sourceRunId, sessionKey) {
632
+ if (!sourceRunId) {
633
+ return undefined;
903
634
  }
904
- return {
905
- toolName,
906
- phase: "streaming",
907
- text: `${toolName}: ${detail}`,
908
- };
909
- }
910
- function normalizeHermesToolName(rawName) {
911
- const normalized = rawName
912
- .trim()
913
- .replace(/[^A-Za-z0-9_.-]+/g, "_")
914
- .replace(/_tools$/i, "")
915
- .replace(/_tool$/i, "");
916
- return normalized || "tool";
635
+ // 这个块是 ClawConnect 和 Hermes history 的稳定身份合同;客户端展示时会剥离它。
636
+ return [
637
+ "[ClawConnect mobile turn]",
638
+ `sourceRunId: ${sourceRunId}`,
639
+ `sessionKey: ${sessionKey}`,
640
+ ].join("\n");
917
641
  }
918
- async function prepareHermesMessage(message, attachments, sessionKey) {
642
+ async function prepareHermesMessage(message, attachments, sessionKey, sourceRunId, preparationPlan = planHermesChatPreparation(message)) {
919
643
  const refs = [];
920
644
  if (Array.isArray(attachments)) {
921
645
  const safeSession = sessionKey.replace(/[^\w.-]/g, "_") || "main";
@@ -938,16 +662,61 @@ async function prepareHermesMessage(message, attachments, sessionKey) {
938
662
  refs.push(`[file attached: ${filePath} (${mimeType})]`);
939
663
  }
940
664
  }
941
- const sections = [message.trim()];
665
+ const userSections = [message.trim()];
942
666
  if (refs.length > 0) {
943
- sections.push(refs.join("\n"));
944
- }
945
- const runtimeHint = buildHermesRuntimeContextHint(readHermesStatusSnapshot());
946
- if (runtimeHint) {
947
- sections.push(runtimeHint);
948
- }
949
- sections.push(CLAWCONNECT_MOBILE_BRIDGE_HINT);
950
- return sections.filter(Boolean).join("\n\n").trim();
667
+ userSections.push(refs.join("\n"));
668
+ }
669
+ const runtimeHint = buildHermesRuntimeContextHint(readCachedHermesStatusSnapshot(HERMES_RUNTIME_CONTEXT_CACHE_MAX_AGE_MS));
670
+ const turnMetadata = buildClawConnectMobileTurnMetadata(sourceRunId, sessionKey);
671
+ const apiMessageSections = [...userSections];
672
+ if (turnMetadata) {
673
+ apiMessageSections.push(turnMetadata);
674
+ }
675
+ const apiInstructionSections = [
676
+ runtimeHint,
677
+ CLAWCONNECT_MOBILE_BRIDGE_HINT,
678
+ turnMetadata ? CLAWCONNECT_MOBILE_TURN_INSTRUCTION : undefined,
679
+ ];
680
+ const cliMessageSections = [
681
+ ...userSections,
682
+ runtimeHint,
683
+ CLAWCONNECT_MOBILE_BRIDGE_HINT,
684
+ turnMetadata
685
+ ? [turnMetadata, CLAWCONNECT_MOBILE_TURN_INSTRUCTION].join("\n")
686
+ : undefined,
687
+ ];
688
+ return {
689
+ apiMessage: apiMessageSections.filter(Boolean).join("\n\n").trim(),
690
+ apiInstructions: apiInstructionSections.filter(Boolean).join("\n\n").trim() || undefined,
691
+ cliMessage: cliMessageSections.filter(Boolean).join("\n\n").trim(),
692
+ };
693
+ }
694
+ function planHermesChatPreparation(message) {
695
+ return {
696
+ preloadFileTransferSkill: shouldPreloadHermesFileTransferSkill(message),
697
+ };
698
+ }
699
+ function shouldPreloadHermesFileTransferSkill(message) {
700
+ const text = message.trim();
701
+ const lower = text.toLowerCase();
702
+ if (!text) {
703
+ return false;
704
+ }
705
+ if (/\bfile-transfer\b|clawconnect\s+send-file/.test(lower)) {
706
+ return true;
707
+ }
708
+ const pathOrExtension = /(?:^|\s|["'“”‘’])(?:~|\/|[a-z]:\\)|\.(?:png|jpe?g|gif|webp|heic|pdf|docx?|xlsx?|pptx?|zip|txt|md)\b/i.test(text);
709
+ const englishSend = /\b(send|transfer|upload|attach|share|copy)\b/i.test(text);
710
+ const englishFile = /\b(file|image|photo|picture|screenshot|document|attachment)\b/i.test(text) || pathOrExtension;
711
+ const englishTarget = /\b(phone|iphone|mobile|clawlink|this device|me)\b/i.test(text);
712
+ if (englishSend && englishFile && englishTarget) {
713
+ return true;
714
+ }
715
+ const chineseSend = /发送|发给|发到|发过来|传到|传给|传输|上传|分享|转发|给我|发/.test(text);
716
+ const chineseFile = /文件|图片|照片|截图|文档|附件|桌面|本机|路径|微信图片/.test(text) || pathOrExtension;
717
+ const chineseTarget = /手机|移动端|这台|给我|发过来|iPhone|ClawLink/i.test(text);
718
+ // 文件传输技能会触发 Hermes CLI 的 skills list;普通文本必须不走这条慢路径。
719
+ return chineseSend && chineseFile && chineseTarget;
951
720
  }
952
721
  export function buildHermesRuntimeContextHint(snapshot) {
953
722
  const details = compactStringArray([