clawconnect-agent 0.2.12 → 0.2.14

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 (83) hide show
  1. package/dist/commands/send-file.d.ts +2 -0
  2. package/dist/commands/send-file.js +8 -0
  3. package/dist/commands/send-file.js.map +1 -1
  4. package/dist/core/relay/source-commit-publish-flow.d.ts +36 -0
  5. package/dist/core/relay/source-commit-publish-flow.js +107 -0
  6. package/dist/core/relay/source-commit-publish-flow.js.map +1 -0
  7. package/dist/core/relay/source-commit.d.ts +7 -0
  8. package/dist/core/relay/source-commit.js +25 -1
  9. package/dist/core/relay/source-commit.js.map +1 -1
  10. package/dist/core/thinking-level.d.ts +45 -0
  11. package/dist/core/thinking-level.js +58 -0
  12. package/dist/core/thinking-level.js.map +1 -0
  13. package/dist/hermes/hermes-relay-manager.d.ts +3 -0
  14. package/dist/hermes/hermes-relay-manager.js +7 -0
  15. package/dist/hermes/hermes-relay-manager.js.map +1 -1
  16. package/dist/hermes/hermes-session-reasoning-store.d.ts +13 -0
  17. package/dist/hermes/hermes-session-reasoning-store.js +99 -0
  18. package/dist/hermes/hermes-session-reasoning-store.js.map +1 -0
  19. package/dist/hermes/relay/hermes-cron-change-relay.d.ts +28 -0
  20. package/dist/hermes/relay/hermes-cron-change-relay.js +55 -0
  21. package/dist/hermes/relay/hermes-cron-change-relay.js.map +1 -0
  22. package/dist/hermes/runtime/hermes-cron-change-watcher.d.ts +34 -0
  23. package/dist/hermes/runtime/hermes-cron-change-watcher.js +135 -0
  24. package/dist/hermes/runtime/hermes-cron-change-watcher.js.map +1 -0
  25. package/dist/hermes/runtime/hermes-runtime-api-client.d.ts +10 -0
  26. package/dist/hermes/runtime/hermes-runtime-api-client.js +7 -4
  27. package/dist/hermes/runtime/hermes-runtime-api-client.js.map +1 -1
  28. package/dist/hermes/runtime/hermes-runtime-api-settings.d.ts +6 -0
  29. package/dist/hermes/runtime/hermes-runtime-api-settings.js +18 -12
  30. package/dist/hermes/runtime/hermes-runtime-api-settings.js.map +1 -1
  31. package/dist/hermes/runtime/hermes-runtime-chat-cli.d.ts +16 -0
  32. package/dist/hermes/runtime/hermes-runtime-chat-cli.js +359 -0
  33. package/dist/hermes/runtime/hermes-runtime-chat-cli.js.map +1 -0
  34. package/dist/hermes/runtime/hermes-runtime-chat-file-transfer.d.ts +29 -0
  35. package/dist/hermes/runtime/hermes-runtime-chat-file-transfer.js +323 -0
  36. package/dist/hermes/runtime/hermes-runtime-chat-file-transfer.js.map +1 -0
  37. package/dist/hermes/runtime/hermes-runtime-chat-message.d.ts +10 -0
  38. package/dist/hermes/runtime/hermes-runtime-chat-message.js +133 -0
  39. package/dist/hermes/runtime/hermes-runtime-chat-message.js.map +1 -0
  40. package/dist/hermes/runtime/hermes-runtime-chat-output.d.ts +17 -0
  41. package/dist/hermes/runtime/hermes-runtime-chat-output.js +90 -0
  42. package/dist/hermes/runtime/hermes-runtime-chat-output.js.map +1 -0
  43. package/dist/hermes/runtime/hermes-runtime-chat-queue.d.ts +1 -0
  44. package/dist/hermes/runtime/hermes-runtime-chat-queue.js +18 -0
  45. package/dist/hermes/runtime/hermes-runtime-chat-queue.js.map +1 -0
  46. package/dist/hermes/runtime/hermes-runtime-chat.d.ts +3 -33
  47. package/dist/hermes/runtime/hermes-runtime-chat.js +47 -908
  48. package/dist/hermes/runtime/hermes-runtime-chat.js.map +1 -1
  49. package/dist/hermes/runtime/hermes-runtime-command-router.js +6 -1
  50. package/dist/hermes/runtime/hermes-runtime-command-router.js.map +1 -1
  51. package/dist/hermes/runtime/hermes-runtime-reasoning.d.ts +29 -0
  52. package/dist/hermes/runtime/hermes-runtime-reasoning.js +72 -0
  53. package/dist/hermes/runtime/hermes-runtime-reasoning.js.map +1 -0
  54. package/dist/hermes/runtime/hermes-runtime-sessions.d.ts +2 -2
  55. package/dist/hermes/runtime/hermes-runtime-sessions.js +23 -2
  56. package/dist/hermes/runtime/hermes-runtime-sessions.js.map +1 -1
  57. package/dist/hermes/runtime/hermes-runtime-slash-command.d.ts +14 -1
  58. package/dist/hermes/runtime/hermes-runtime-slash-command.js +6 -1
  59. package/dist/hermes/runtime/hermes-runtime-slash-command.js.map +1 -1
  60. package/dist/hermes/runtime/hermes-runtime-thinking.d.ts +14 -0
  61. package/dist/hermes/runtime/hermes-runtime-thinking.js +105 -0
  62. package/dist/hermes/runtime/hermes-runtime-thinking.js.map +1 -0
  63. package/dist/openclaw/handlers/thinking-handlers.d.ts +22 -0
  64. package/dist/openclaw/handlers/thinking-handlers.js +104 -0
  65. package/dist/openclaw/handlers/thinking-handlers.js.map +1 -0
  66. package/dist/openclaw/relay/chat-history.d.ts +5 -1
  67. package/dist/openclaw/relay/chat-history.js +18 -18
  68. package/dist/openclaw/relay/chat-history.js.map +1 -1
  69. package/dist/openclaw/relay/openclaw-source-commit-watcher.d.ts +12 -4
  70. package/dist/openclaw/relay/openclaw-source-commit-watcher.js +13 -2
  71. package/dist/openclaw/relay/openclaw-source-commit-watcher.js.map +1 -1
  72. package/dist/openclaw/relay/openclaw-source-cursor-connections.d.ts +25 -0
  73. package/dist/openclaw/relay/openclaw-source-cursor-connections.js +77 -0
  74. package/dist/openclaw/relay/openclaw-source-cursor-connections.js.map +1 -0
  75. package/dist/openclaw/relay/source-commit-projection.d.ts +28 -6
  76. package/dist/openclaw/relay/source-commit-projection.js +106 -30
  77. package/dist/openclaw/relay/source-commit-projection.js.map +1 -1
  78. package/dist/openclaw/relay/source-row-fingerprints.d.ts +22 -0
  79. package/dist/openclaw/relay/source-row-fingerprints.js +48 -0
  80. package/dist/openclaw/relay/source-row-fingerprints.js.map +1 -0
  81. package/dist/openclaw/relay-manager.js +68 -24
  82. package/dist/openclaw/relay-manager.js.map +1 -1
  83. package/package.json +1 -1
@@ -1,43 +1,29 @@
1
- import { randomUUID } from "crypto";
2
- import { mkdir, readdir, rm, stat, writeFile } from "fs/promises";
3
- import { join } from "path";
4
- import { spawn } from "child_process";
5
1
  import { buildMobileAssistantDeltaPayload, } from "../../core/relay/mobile-chat-run-bridge.js";
6
- import { buildToolInvocationUpdatedEvent, } from "../../core/relay/timeline-event-builder.js";
7
2
  import { forgetHermesSession, getMappedHermesSessionId, rememberHermesSession, } from "../hermes-session-store.js";
8
- import { CHAT_TIMEOUT_MS, CLAWCONNECT_MOBILE_BRIDGE_HINT, HERMES_INBOX_DIR, SUBPROCESS_ENV, hermesInvocation, 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, stringValue } from "./hermes-runtime-values.js";
11
- import { detectHermesHistoryCompletion, hermesSourceRunIdFromMessage, selectHermesSessionForCompletedChat, } from "./hermes-runtime-history-completion.js";
12
- import { captureHermesSessionActiveHead, readHermesSessionMessages, rewindHermesSessionAfterActiveHead, } from "./hermes-runtime-state-db.js";
3
+ import { isHermesMissingSessionError, sanitizeHermesChatOutput, } from "./hermes-runtime-process.js";
4
+ import { collectHermesUsageSnapshot, listHermesSessions, readHermesStatusSnapshotAsync, } from "./hermes-runtime-usage.js";
5
+ import { detectHermesHistoryCompletion, selectHermesSessionForCompletedChat, } from "./hermes-runtime-history-completion.js";
6
+ import { captureHermesSessionActiveHead } from "./hermes-runtime-state-db.js";
13
7
  import { ensureHermesApiSessionForMobileRoute, tryRunHermesApiChat } from "./hermes-runtime-api-client.js";
14
8
  import { resolveHermesPreloadedSkillContext } from "./hermes-runtime-preloaded-skills.js";
15
- import { createHermesToolLogWatcher, hermesToolState, } from "./hermes-runtime-tool-log-watcher.js";
9
+ import { resolveHermesSessionReasoningEffort } from "./hermes-runtime-reasoning.js";
16
10
  import { isHermesSlashCommandMessage, runHermesSlashCommand, } from "./hermes-runtime-slash-command.js";
17
- import { clearPendingHermesFileTransfer, getPendingHermesFileTransfer, recordPendingHermesFileTransfer, } from "./hermes-file-transfer-state.js";
18
- import { clearHermesMobileFileRoute, registerHermesMobileFileRoute, } from "./hermes-mobile-file-route-store.js";
11
+ import { runSerializedHermesChat } from "./hermes-runtime-chat-queue.js";
12
+ import { prepareRequiredHermesMessage, } from "./hermes-runtime-chat-message.js";
13
+ import { planHermesChatPreparation, verifyHermesFileTransferIfRequired, } from "./hermes-runtime-chat-file-transfer.js";
14
+ import { isHermesChatAbortedError, recoverEmptyHermesApiOutputFromHistory, recoverHermesCliSessionAfterAbort, requireVisibleHermesOutput, } from "./hermes-runtime-chat-output.js";
15
+ import { runHermesChatOnceWithMobileFileRoute } from "./hermes-runtime-chat-cli.js";
19
16
  export { selectHermesSessionForCompletedChat } from "./hermes-runtime-history-completion.js";
20
17
  export { latestTerminalAssistantReplyFromHermesExport } from "./hermes-runtime-history-completion.js";
21
18
  export { parseHermesToolLogLine } from "./hermes-runtime-tool-log-watcher.js";
22
19
  export { isHermesSlashCommandMessage } from "./hermes-runtime-slash-command.js";
23
- const HERMES_HISTORY_COMPLETION_GRACE_MS = 2_000;
24
- const HERMES_HISTORY_COMPLETION_POLL_MS = 1_000;
25
- const HERMES_API_EMPTY_OUTPUT_HISTORY_COMPLETION_TIMEOUT_MS = 12_000;
26
- const HERMES_API_EMPTY_OUTPUT_HISTORY_COMPLETION_POLL_MS = 500;
27
- const HERMES_RUNTIME_CONTEXT_CACHE_MAX_AGE_MS = 5 * 60_000;
28
- const HERMES_COMMAND_DENIED_TIMEOUT_MESSAGE = "Timeout – denying command";
29
- const HERMES_EMPTY_RESPONSE_MESSAGE = "Hermes 未返回可见回复,请检查当前模型额度或 Provider 凭据后重试。";
30
- const HERMES_INBOX_TTL_MS = 24 * 60 * 60 * 1000;
31
- const USER_FILE_MARKER_PREFIX_RE = /\[file attached:/gi;
32
- const hermesChatQueues = new Map();
20
+ export { buildHermesRuntimeContextHint, cleanupExpiredHermesInbox, sanitizeHermesUserAttachmentMarkers, } from "./hermes-runtime-chat-message.js";
21
+ export { collectHermesFileTransferEvidence, collectHermesFileTransferOutcome, planHermesChatPreparation, verifyHermesFileTransferIfRequired, } from "./hermes-runtime-chat-file-transfer.js";
33
22
  const EMPTY_PRELOADED_SKILL_CONTEXT = {
34
23
  cliArgs: [],
35
24
  requiredToolsets: [],
36
25
  skillNames: [],
37
26
  };
38
- const HERMES_FILE_TRANSFER_NOT_SENT_MESSAGE = "文件尚未发送:没有检测到可验证的 clawconnect send-file 成功结果,请补充或确认要发送的文件后重试。";
39
- const HERMES_FILE_TRANSFER_CONTENT_MISSING_MESSAGE = "本轮没有可显示的结构化回答。";
40
- const HERMES_FILE_TRANSFER_CANCELLED_MESSAGE = "文件发送已取消。";
41
27
  export async function runHermesChat(params, context = {}) {
42
28
  const record = params && typeof params === "object" && !Array.isArray(params)
43
29
  ? params
@@ -51,20 +37,21 @@ export async function runHermesChat(params, context = {}) {
51
37
  message: rawMessage,
52
38
  sessionKey,
53
39
  hermesSessionId: record.hermesSessionId,
54
- runQueuedChat: (queuedMessage, hermesSessionId) => runHermesChat({
55
- message: queuedMessage,
56
- sessionKey,
57
- ...(hermesSessionId ? { hermesSessionId } : {}),
40
+ gatewayId: context.gatewayId,
41
+ // 排队对话在斜杠命令持有的会话串行槽位内执行,不能再次进入 runSerializedHermesChat,
42
+ // 否则会等待自身所在的槽位完成而永久死锁。
43
+ // 上下文只携带显式传递的 gatewayId:本轮不是移动端发起的独立请求,不能复用原请求的 requestId。
44
+ runQueuedChat: (queued) => runUnserializedHermesChatTurn({
45
+ rawMessage: queued.message,
46
+ attachments: undefined,
47
+ sessionKey: queued.sessionKey,
48
+ hermesSessionId: queued.hermesSessionId,
49
+ context: { gatewayId: queued.gatewayId },
58
50
  }),
59
51
  })));
60
52
  }
61
- const sourceRunId = typeof context.requestId === "string" && context.requestId.trim().length > 0
62
- ? context.requestId.trim()
63
- : undefined;
64
- const preparedMessage = await prepareHermesMessage(rawMessage, record.attachments, sessionKey, sourceRunId);
65
- if (!preparedMessage.cliMessage.trim()) {
66
- throw new Error("message_required");
67
- }
53
+ const sourceRunId = resolveHermesSourceRunId(context);
54
+ const preparedMessage = await prepareRequiredHermesMessage(rawMessage, record.attachments, sessionKey, sourceRunId);
68
55
  return await runSerializedHermesChat(sessionKey, async () => {
69
56
  return await runHermesChatPrepared({
70
57
  rawMessage,
@@ -76,20 +63,23 @@ export async function runHermesChat(params, context = {}) {
76
63
  });
77
64
  });
78
65
  }
79
- async function runSerializedHermesChat(sessionKey, operation) {
80
- const queueKey = sessionKey.trim() || "main";
81
- const previous = hermesChatQueues.get(queueKey) ?? Promise.resolve();
82
- const current = previous.catch(() => undefined).then(operation);
83
- const stored = current.then(() => undefined, () => undefined);
84
- hermesChatQueues.set(queueKey, stored);
85
- try {
86
- return await current;
87
- }
88
- finally {
89
- if (hermesChatQueues.get(queueKey) === stored) {
90
- hermesChatQueues.delete(queueKey);
91
- }
92
- }
66
+ /** 调用方必须已持有该 sessionKey 的串行槽位。 */
67
+ async function runUnserializedHermesChatTurn(params) {
68
+ const sourceRunId = resolveHermesSourceRunId(params.context);
69
+ const preparedMessage = await prepareRequiredHermesMessage(params.rawMessage, params.attachments, params.sessionKey, sourceRunId);
70
+ return await runHermesChatPrepared({
71
+ rawMessage: params.rawMessage,
72
+ preparedMessage,
73
+ sessionKey: params.sessionKey,
74
+ sourceRunId,
75
+ hermesSessionId: params.hermesSessionId,
76
+ context: params.context,
77
+ });
78
+ }
79
+ function resolveHermesSourceRunId(context) {
80
+ return typeof context.requestId === "string" && context.requestId.trim().length > 0
81
+ ? context.requestId.trim()
82
+ : undefined;
93
83
  }
94
84
  async function runHermesChatPrepared(params) {
95
85
  const explicitResume = typeof params.hermesSessionId === "string" && params.hermesSessionId.trim().length > 0
@@ -108,6 +98,8 @@ async function runHermesChatPrepared(params) {
108
98
  const preloadedSkillContext = preparationPlan.preloadFileTransferSkill
109
99
  ? await resolveHermesPreloadedSkillContext({ forceFileTransfer: true })
110
100
  : EMPTY_PRELOADED_SKILL_CONTEXT;
101
+ // 会话级思考等级覆盖在本轮开始时一次性解析,API 与 CLI(含重试)使用同一值。
102
+ const reasoningEffort = await resolveHermesSessionReasoningEffort(params.context.gatewayId, params.sessionKey);
111
103
  if (preparationPlan.preloadFileTransferSkill && !resume && params.sourceRunId) {
112
104
  const ensuredSessionId = await ensureHermesApiSessionForMobileRoute(params.sessionKey);
113
105
  if (ensuredSessionId) {
@@ -134,6 +126,7 @@ async function runHermesChatPrepared(params) {
134
126
  resume,
135
127
  preloadedSkillNames: preloadedSkillContext.skillNames,
136
128
  requiredToolsets: preloadedSkillContext.requiredToolsets,
129
+ reasoningEffort,
137
130
  context: params.context,
138
131
  });
139
132
  if (apiChat) {
@@ -179,6 +172,7 @@ async function runHermesChatPrepared(params) {
179
172
  sessionKey: params.sessionKey,
180
173
  preloadedSkillNames: preloadedSkillContext.skillNames,
181
174
  requiredToolsets: preloadedSkillContext.requiredToolsets,
175
+ reasoningEffort,
182
176
  context: params.context,
183
177
  });
184
178
  if (retryApiChat) {
@@ -217,6 +211,7 @@ async function runHermesChatPrepared(params) {
217
211
  resume,
218
212
  context: params.context,
219
213
  preloadedSkillArgs: preloadedSkillContext.cliArgs,
214
+ reasoningEffort,
220
215
  enableFileTransferRoute: preparationPlan.preloadFileTransferSkill,
221
216
  gatewayId: params.context.gatewayId ?? "clawconnect",
222
217
  sourceRunId: params.sourceRunId,
@@ -251,6 +246,7 @@ async function runHermesChatPrepared(params) {
251
246
  sessionKey: params.sessionKey,
252
247
  context: params.context,
253
248
  preloadedSkillArgs: preloadedSkillContext.cliArgs,
249
+ reasoningEffort,
254
250
  enableFileTransferRoute: preparationPlan.preloadFileTransferSkill,
255
251
  gatewayId: params.context.gatewayId ?? "clawconnect",
256
252
  sourceRunId: params.sourceRunId,
@@ -292,426 +288,6 @@ async function runHermesChatPrepared(params) {
292
288
  ...verifiedFileTransfer,
293
289
  };
294
290
  }
295
- function isHermesChatAbortedError(error) {
296
- return error instanceof Error && error.message === "hermes_chat_aborted";
297
- }
298
- async function recoverHermesCliSessionAfterAbort(params) {
299
- if (!params.resume) {
300
- return;
301
- }
302
- // Hermes CLI writes the new user row before it reacts to SIGTERM. Rewind
303
- // only rows beyond the stable pre-run head so completed context remains
304
- // resumable and the canceled row cannot be answered on the next request.
305
- if (params.activeHead !== undefined && params.sourceRunId) {
306
- const recovered = await rewindHermesSessionAfterActiveHead(params.resume, params.activeHead, params.sourceRunId);
307
- if (recovered) {
308
- return;
309
- }
310
- }
311
- // Old/unavailable state.db schemas cannot provide a safe row boundary. For
312
- // an implicit alias, fail closed by dropping only that alias instead of
313
- // risking a replay of the canceled prompt. Explicit Hermes session ids are
314
- // never silently deleted or remapped.
315
- if (params.mappedResume !== params.resume) {
316
- return;
317
- }
318
- try {
319
- await forgetHermesSession(params.sessionKey, params.mappedResume);
320
- }
321
- catch {
322
- // Abort delivery must not be replaced by a best-effort recovery failure.
323
- }
324
- }
325
- function requireVisibleHermesOutput(output) {
326
- const visibleOutput = sanitizeHermesChatOutput(output).trim();
327
- if (!visibleOutput) {
328
- // 空完成会让移动端永久留下无内容回复;没有可见文本时必须走显式失败事件。
329
- throw new Error(HERMES_EMPTY_RESPONSE_MESSAGE);
330
- }
331
- if (isHermesProviderFailureOutput(visibleOutput)) {
332
- // Hermes 某些 Provider 会以 exit 0 返回错误文本;这仍是失败,不能渲染成正常 assistant 回复。
333
- throw new Error(visibleOutput);
334
- }
335
- return visibleOutput;
336
- }
337
- function isHermesProviderFailureOutput(output) {
338
- const firstLine = output.split(/\r?\n/, 1)[0]?.trim() || "";
339
- return /^(?:[❌✕x]\s*)?API call failed(?: after \d+ retries)?\s*:/i.test(firstLine)
340
- || /^(?:[❌✕x]\s*)?HTTP\s+(?:401|402|403|429|5\d\d)\b/i.test(firstLine)
341
- || /^Error code:\s*(?:401|402|403|429|5\d\d)\b/i.test(firstLine);
342
- }
343
- async function recoverEmptyHermesApiOutputFromHistory(params) {
344
- if (sanitizeHermesChatOutput(params.output).trim()) {
345
- return undefined;
346
- }
347
- if (!params.hermesSessionId?.trim()) {
348
- return undefined;
349
- }
350
- const deadline = Date.now() + HERMES_API_EMPTY_OUTPUT_HISTORY_COMPLETION_TIMEOUT_MS;
351
- while (Date.now() <= deadline) {
352
- if (params.abortSignal?.aborted) {
353
- throw new Error("hermes_chat_aborted");
354
- }
355
- const detectedOutput = await detectHermesHistoryCompletion({
356
- beforeSessions: [],
357
- resume: params.hermesSessionId,
358
- sessionKey: params.sessionKey,
359
- userMessage: params.userMessage,
360
- sourceRunId: params.sourceRunId,
361
- });
362
- if (detectedOutput) {
363
- return detectedOutput;
364
- }
365
- const remainingMs = deadline - Date.now();
366
- if (remainingMs <= 0) {
367
- break;
368
- }
369
- await sleep(Math.min(HERMES_API_EMPTY_OUTPUT_HISTORY_COMPLETION_POLL_MS, remainingMs));
370
- }
371
- return undefined;
372
- }
373
- async function sleep(ms) {
374
- await new Promise((resolve) => setTimeout(resolve, ms));
375
- }
376
- async function runHermesChatOnce(params) {
377
- const args = [
378
- "chat",
379
- // Hermes 会按 display.interface 自动切到 TUI;移动端桥接必须强制经典 CLI,
380
- // 再由 --quiet 只输出最终回答。Tools 过程仍由 agent.log watcher 独立发布。
381
- "--cli",
382
- "--query",
383
- params.message,
384
- "--quiet",
385
- "--source",
386
- "pocketclaw",
387
- ...(params.preloadedSkillArgs ?? []),
388
- "--yolo",
389
- ];
390
- if (params.resume) {
391
- args.push("--resume", params.resume);
392
- }
393
- const runId = params.context.requestId ?? `hermes-${Date.now()}`;
394
- const env = hermesChatSubprocessEnv(runId, params.sessionKey);
395
- return params.context.publishEvent
396
- ? await runHermesChatStreaming(args, params.sessionKey, params.context, runId, env, params.historyCompletion)
397
- : runHermes(args, CHAT_TIMEOUT_MS, env);
398
- }
399
- async function runHermesChatOnceWithMobileFileRoute(params) {
400
- const hermesSessionId = params.resume?.trim();
401
- const sourceRunId = params.sourceRunId?.trim();
402
- if (!params.enableFileTransferRoute || !hermesSessionId || !sourceRunId) {
403
- return await runHermesChatOnce(params);
404
- }
405
- await registerHermesMobileFileRoute({
406
- hermesSessionId,
407
- gatewayId: params.gatewayId,
408
- sessionKey: params.sessionKey,
409
- sourceRunId,
410
- });
411
- try {
412
- return await runHermesChatOnce(params);
413
- }
414
- finally {
415
- await clearHermesMobileFileRoute(hermesSessionId, sourceRunId);
416
- }
417
- }
418
- async function runHermesChatStreaming(args, sessionKey, context, runId, env, historyCompletion) {
419
- const invocation = hermesInvocation(args);
420
- const child = spawn(invocation.command, invocation.args, {
421
- env,
422
- stdio: ["ignore", "pipe", "pipe"],
423
- windowsHide: true,
424
- });
425
- let output = "";
426
- let stderr = "";
427
- let seq = 0;
428
- let stdoutLineBuffer = "";
429
- let inSecurityReview = false;
430
- let commandDeniedTimeout = false;
431
- let commandDeniedTimeoutKillTimer;
432
- const toolCallIdsByName = new Map();
433
- let toolCallCounter = 0;
434
- const requestCommandDeniedTimeoutFailure = () => {
435
- if (commandDeniedTimeout) {
436
- return;
437
- }
438
- commandDeniedTimeout = true;
439
- child.kill("SIGTERM");
440
- commandDeniedTimeoutKillTimer = setTimeout(() => child.kill("SIGKILL"), 1000);
441
- commandDeniedTimeoutKillTimer.unref?.();
442
- };
443
- const publishToolLogEvent = (event) => {
444
- let toolCallId = toolCallIdsByName.get(event.toolName);
445
- if (!toolCallId) {
446
- toolCallCounter += 1;
447
- toolCallId = `${runId}:hermes-tool-${toolCallCounter}`;
448
- toolCallIdsByName.set(event.toolName, toolCallId);
449
- }
450
- if (event.phase === "completed" || event.phase === "failed") {
451
- toolCallIdsByName.delete(event.toolName);
452
- }
453
- context.publishEvent?.({
454
- type: "event",
455
- event: "chat",
456
- payload: {
457
- runId,
458
- sessionKey,
459
- stream: "tool",
460
- state: event.phase,
461
- phase: event.phase,
462
- role: "tool",
463
- seq: seq += 1,
464
- ts: Date.now(),
465
- data: {
466
- phase: event.phase,
467
- tool_call_id: toolCallId,
468
- tool_name: event.toolName,
469
- text: event.text,
470
- is_error: event.isError === true,
471
- },
472
- timelineEvents: [
473
- buildToolInvocationUpdatedEvent({
474
- gatewayId: context.gatewayId ?? "clawconnect",
475
- sessionKey,
476
- turnId: runId,
477
- runId,
478
- toolInvocationId: toolCallId,
479
- toolState: hermesToolState(event),
480
- seq: seq,
481
- turnSeq: seq,
482
- content: [{
483
- type: event.phase === "completed" || event.phase === "failed" ? "tool_result" : "tool_call",
484
- toolName: event.toolName,
485
- text: event.text,
486
- isError: event.isError === true,
487
- }],
488
- }),
489
- ],
490
- },
491
- });
492
- };
493
- const toolLogWatcher = createHermesToolLogWatcher(publishToolLogEvent);
494
- const filterChatLine = (line) => {
495
- const clean = stripAnsi(line).trim();
496
- if (isHermesCommandDeniedTimeoutLine(clean)) {
497
- requestCommandDeniedTimeoutFailure();
498
- return null;
499
- }
500
- if (/DANGEROUS COMMAND:\s*Security scan/i.test(clean)) {
501
- inSecurityReview = true;
502
- return null;
503
- }
504
- if (inSecurityReview) {
505
- if (/Choice\s*\[[^\]]+\]:/i.test(clean) || /(?:^|\s)[✕x]\s*Denied\b/i.test(clean) || /\bDenied\b/i.test(clean)) {
506
- inSecurityReview = false;
507
- }
508
- return null;
509
- }
510
- return line;
511
- };
512
- const appendStdout = (text) => {
513
- // Hermes CLI stdout is a presentation stream, not a semantic assistant stream.
514
- // Some Windows builds emit transient TUI frames (Reasoning boxes, spinners, etc.)
515
- // even with --cli --quiet. Buffer it for terminal completion, but never expose it
516
- // as message.part.delta. The API path has typed assistant.delta events and remains
517
- // the only Hermes path allowed to stream assistant text.
518
- output += text;
519
- };
520
- const flushStdoutLineBuffer = () => {
521
- if (!stdoutLineBuffer) {
522
- return;
523
- }
524
- const clean = stripHermesSessionResumeNotices(filterChatLine(stdoutLineBuffer) ?? "");
525
- stdoutLineBuffer = "";
526
- if (!clean.trim()) {
527
- return;
528
- }
529
- appendStdout(clean);
530
- };
531
- const publishText = (text) => {
532
- stdoutLineBuffer += text;
533
- const lines = stdoutLineBuffer.split(/\r?\n/);
534
- stdoutLineBuffer = lines.pop() ?? "";
535
- const clean = stripHermesSessionResumeNotices(lines
536
- .map(filterChatLine)
537
- .filter((line) => line !== null)
538
- .join("\n"));
539
- if (!clean.trim()) {
540
- return;
541
- }
542
- const chunk = `${clean}\n`;
543
- appendStdout(chunk);
544
- };
545
- const publishStderr = (text) => {
546
- const clean = stripAnsi(text).trimEnd();
547
- if (!clean) {
548
- return;
549
- }
550
- stderr += `${clean}\n`;
551
- context.publishEvent?.({
552
- type: "event",
553
- event: "maintenance_log",
554
- payload: {
555
- gatewayId: context.gatewayId,
556
- requestId: context.requestId,
557
- runId,
558
- stream: "stderr",
559
- seq: seq += 1,
560
- ts: Date.now(),
561
- text: clean,
562
- },
563
- });
564
- };
565
- const publishTypingMarker = () => {
566
- // The local mobile client already owns the pending placeholder. Do not
567
- // forward protocol-only typing markers as empty assistant chat events.
568
- };
569
- child.stdout?.on("data", (chunk) => publishText(chunk.toString()));
570
- child.stderr?.on("data", (chunk) => publishStderr(chunk.toString()));
571
- toolLogWatcher.start();
572
- publishTypingMarker();
573
- return await new Promise((resolveOutput, rejectOutput) => {
574
- const abortSignal = context.abortSignal;
575
- let abortRequested = abortSignal?.aborted === true;
576
- let settled = false;
577
- let historyCompletionTimer;
578
- let historyCompletionInFlight = false;
579
- const typingTimer = setInterval(publishTypingMarker, 5000);
580
- typingTimer.unref?.();
581
- const cleanup = () => {
582
- clearInterval(typingTimer);
583
- if (historyCompletionTimer) {
584
- clearInterval(historyCompletionTimer);
585
- historyCompletionTimer = undefined;
586
- }
587
- abortSignal?.removeEventListener("abort", abortChat);
588
- };
589
- const finishResolve = (value) => {
590
- if (settled) {
591
- return;
592
- }
593
- settled = true;
594
- cleanup();
595
- clearTimeout(timeout);
596
- toolLogWatcher.stop();
597
- resolveOutput(value);
598
- };
599
- const finishReject = (error) => {
600
- if (settled) {
601
- return;
602
- }
603
- settled = true;
604
- cleanup();
605
- clearTimeout(timeout);
606
- toolLogWatcher.stop();
607
- rejectOutput(error);
608
- };
609
- const timeout = setTimeout(() => {
610
- child.kill("SIGTERM");
611
- if (output.trim()) {
612
- finishResolve(output);
613
- }
614
- else {
615
- finishReject(new Error("hermes_chat_timeout"));
616
- }
617
- }, CHAT_TIMEOUT_MS);
618
- timeout.unref?.();
619
- const abortChat = () => {
620
- abortRequested = true;
621
- stdoutLineBuffer = "";
622
- child.kill("SIGTERM");
623
- };
624
- const checkHistoryCompletion = () => {
625
- if (!historyCompletion || settled || abortRequested || historyCompletionInFlight) {
626
- return;
627
- }
628
- historyCompletionInFlight = true;
629
- historyCompletion()
630
- .then((detectedOutput) => {
631
- if (!detectedOutput || settled || abortRequested) {
632
- return;
633
- }
634
- output = detectedOutput;
635
- stdoutLineBuffer = "";
636
- child.kill("SIGTERM");
637
- setTimeout(() => {
638
- if (child.exitCode === null && child.signalCode === null) {
639
- child.kill("SIGKILL");
640
- }
641
- }, 1000).unref?.();
642
- finishResolve(detectedOutput);
643
- })
644
- .catch(() => {
645
- // History completion is a best-effort escape hatch for Hermes processes
646
- // that keep running after the assistant turn is already persisted.
647
- })
648
- .finally(() => {
649
- historyCompletionInFlight = false;
650
- });
651
- };
652
- if (abortRequested) {
653
- abortChat();
654
- }
655
- else {
656
- abortSignal?.addEventListener("abort", abortChat, { once: true });
657
- }
658
- if (historyCompletion) {
659
- historyCompletionTimer = setInterval(checkHistoryCompletion, HERMES_HISTORY_COMPLETION_POLL_MS);
660
- setTimeout(checkHistoryCompletion, HERMES_HISTORY_COMPLETION_GRACE_MS);
661
- }
662
- child.once("error", (error) => {
663
- if (settled) {
664
- return;
665
- }
666
- if (commandDeniedTimeoutKillTimer) {
667
- clearTimeout(commandDeniedTimeoutKillTimer);
668
- commandDeniedTimeoutKillTimer = undefined;
669
- }
670
- finishReject(error);
671
- });
672
- child.once("close", (code, signal) => {
673
- void (async () => {
674
- if (settled) {
675
- return;
676
- }
677
- if (commandDeniedTimeoutKillTimer) {
678
- clearTimeout(commandDeniedTimeoutKillTimer);
679
- commandDeniedTimeoutKillTimer = undefined;
680
- }
681
- if (abortRequested) {
682
- finishReject(new Error("hermes_chat_aborted"));
683
- return;
684
- }
685
- flushStdoutLineBuffer();
686
- if (commandDeniedTimeout) {
687
- finishReject(new Error(HERMES_COMMAND_DENIED_TIMEOUT_MESSAGE));
688
- return;
689
- }
690
- if (code && code !== 0) {
691
- const reason = stderr.trim() || output.trim() || `hermes chat exited with code ${code}`;
692
- finishReject(new Error(signal ? `${reason} (${signal})` : reason));
693
- return;
694
- }
695
- if (historyCompletion) {
696
- try {
697
- // Prefer the persisted assistant message over CLI presentation stdout.
698
- // Hermes writes the semantic answer before a normal process exit, while
699
- // stdout may still contain transient TUI frames on Windows.
700
- const persistedOutput = await historyCompletion();
701
- if (persistedOutput) {
702
- output = persistedOutput;
703
- }
704
- }
705
- catch {
706
- // Older/test Hermes installations may not expose readable history.
707
- // In that case --cli --quiet stdout remains the compatibility fallback.
708
- }
709
- }
710
- finishResolve(output);
711
- })().catch((error) => finishReject(error instanceof Error ? error : new Error(String(error))));
712
- });
713
- });
714
- }
715
291
  export function buildHermesAssistantDeltaPayload(params) {
716
292
  return buildMobileAssistantDeltaPayload({
717
293
  run: { runId: params.runId, sessionKey: params.sessionKey },
@@ -721,441 +297,4 @@ export function buildHermesAssistantDeltaPayload(params) {
721
297
  includeTimelineEvents: true,
722
298
  });
723
299
  }
724
- function hermesChatSubprocessEnv(runId, sessionKey) {
725
- return {
726
- ...SUBPROCESS_ENV,
727
- CLAWCONNECT_SOURCE_RUN_ID: runId,
728
- CLAWCONNECT_SESSION_KEY: sessionKey,
729
- CLAWCONNECT_CHAT_SESSION_KEY: sessionKey,
730
- };
731
- }
732
- 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.";
733
- function buildClawConnectMobileTurnMetadata(sourceRunId, sessionKey) {
734
- if (!sourceRunId) {
735
- return undefined;
736
- }
737
- // 这个块是 ClawConnect 和 Hermes history 的稳定身份合同;客户端展示时会剥离它。
738
- return [
739
- "[ClawConnect mobile turn]",
740
- `sourceRunId: ${sourceRunId}`,
741
- `sessionKey: ${sessionKey}`,
742
- ].join("\n");
743
- }
744
- async function prepareHermesMessage(message, attachments, sessionKey, sourceRunId) {
745
- const refs = [];
746
- if (Array.isArray(attachments)) {
747
- await cleanupExpiredHermesInbox();
748
- const safeSession = sessionKey.replace(/[^\w.-]/g, "_") || "main";
749
- for (const attachment of attachments) {
750
- if (!attachment || typeof attachment !== "object" || Array.isArray(attachment)) {
751
- continue;
752
- }
753
- const record = attachment;
754
- if (typeof record.content !== "string" || record.content.length === 0) {
755
- continue;
756
- }
757
- const fileName = sanitizeFileName(typeof record.fileName === "string" ? record.fileName
758
- : typeof record.name === "string" ? record.name
759
- : `attachment-${randomUUID()}`);
760
- const dir = join(HERMES_INBOX_DIR, safeSession, randomUUID());
761
- await mkdir(dir, { recursive: true });
762
- const filePath = join(dir, fileName);
763
- await writeFile(filePath, Buffer.from(record.content, "base64"));
764
- const mimeType = typeof record.mimeType === "string" ? record.mimeType : "application/octet-stream";
765
- refs.push(`[file attached: ${filePath} (${mimeType})]`);
766
- }
767
- }
768
- const userSections = [sanitizeHermesUserAttachmentMarkers(message).trim()];
769
- if (refs.length > 0) {
770
- userSections.push(refs.join("\n"));
771
- }
772
- const runtimeHint = buildHermesRuntimeContextHint(readCachedHermesStatusSnapshot(HERMES_RUNTIME_CONTEXT_CACHE_MAX_AGE_MS));
773
- const turnMetadata = buildClawConnectMobileTurnMetadata(sourceRunId, sessionKey);
774
- const apiMessageSections = [...userSections];
775
- if (turnMetadata) {
776
- apiMessageSections.push(turnMetadata);
777
- }
778
- const apiInstructionSections = [
779
- runtimeHint,
780
- CLAWCONNECT_MOBILE_BRIDGE_HINT,
781
- turnMetadata ? CLAWCONNECT_MOBILE_TURN_INSTRUCTION : undefined,
782
- ];
783
- const cliMessageSections = [
784
- ...userSections,
785
- runtimeHint,
786
- CLAWCONNECT_MOBILE_BRIDGE_HINT,
787
- turnMetadata
788
- ? [turnMetadata, CLAWCONNECT_MOBILE_TURN_INSTRUCTION].join("\n")
789
- : undefined,
790
- ];
791
- return {
792
- apiMessage: apiMessageSections.filter(Boolean).join("\n\n").trim(),
793
- apiInstructions: apiInstructionSections.filter(Boolean).join("\n\n").trim() || undefined,
794
- cliMessage: cliMessageSections.filter(Boolean).join("\n\n").trim(),
795
- };
796
- }
797
- export function sanitizeHermesUserAttachmentMarkers(message) {
798
- // Hermes 的本地文件提示只能由桥接层生成;让用户输入的同形标记失活,
799
- // 避免把任意 Host 路径伪装成移动端附件交给运行时读取。
800
- return message.replace(USER_FILE_MARKER_PREFIX_RE, "[file attached:");
801
- }
802
- export async function cleanupExpiredHermesInbox(inboxDir = HERMES_INBOX_DIR, nowMs = Date.now(), ttlMs = HERMES_INBOX_TTL_MS) {
803
- let sessions;
804
- try {
805
- sessions = await readdir(inboxDir, { withFileTypes: true });
806
- }
807
- catch {
808
- return;
809
- }
810
- await Promise.all(sessions.filter((entry) => entry.isDirectory() && !entry.isSymbolicLink()).map(async (session) => {
811
- const sessionDir = join(inboxDir, session.name);
812
- let runs;
813
- try {
814
- runs = await readdir(sessionDir, { withFileTypes: true });
815
- }
816
- catch {
817
- return;
818
- }
819
- await Promise.all(runs.filter((entry) => entry.isDirectory() && !entry.isSymbolicLink()).map(async (run) => {
820
- const runDir = join(sessionDir, run.name);
821
- try {
822
- const metadata = await stat(runDir);
823
- if (nowMs - metadata.mtimeMs >= Math.max(0, ttlMs)) {
824
- await rm(runDir, { recursive: true, force: true });
825
- }
826
- }
827
- catch {
828
- // 清理失败不能阻断当前聊天;下一轮发送会再次尝试。
829
- }
830
- }));
831
- }));
832
- }
833
- export async function planHermesChatPreparation(params) {
834
- // A continuation is enabled only by a durable host-side pending state from
835
- // the same gateway/mobile/Hermes session. The user's wording is deliberately
836
- // opaque to this router; Hermes receives the full history and decides what it
837
- // means (including any language, selection, or clarification).
838
- const pending = params.sessionId
839
- ? await getPendingHermesFileTransfer({
840
- gatewayId: params.gatewayId,
841
- sessionKey: params.sessionKey,
842
- hermesSessionId: params.sessionId,
843
- sourceRunId: params.sourceRunId,
844
- })
845
- : undefined;
846
- return {
847
- // The mobile relay explicitly grants the typed CLI file-transfer
848
- // capability on every turn. The model/tool protocol, rather than a
849
- // language-specific intent table, decides whether a send is needed. This
850
- // deliberately trades the API fast path for a buffered CLI path on mobile
851
- // turns, where send-file receipts are persisted atomically.
852
- preloadFileTransferSkill: params.fileTransferCapability === "cli" || pending !== undefined,
853
- fileTransferMode: pending ? "continuation" : undefined,
854
- ...(pending ? { pendingFileTransfer: pending } : {}),
855
- };
856
- }
857
- function normalizeHermesRole(value) {
858
- return typeof value === "string" ? value.trim().toLowerCase() : "";
859
- }
860
- export async function verifyHermesFileTransferIfRequired(params) {
861
- if (!params.plan.preloadFileTransferSkill) {
862
- return {};
863
- }
864
- const messages = params.sessionId ? await readHermesSessionMessages(params.sessionId) : undefined;
865
- const evidence = params.sourceRunId
866
- ? collectHermesFileTransferEvidence(messages, params.sourceRunId)
867
- : [];
868
- if (evidence.length > 0) {
869
- await clearPendingHermesFileTransfer(params.gatewayId, params.sessionKey, params.plan.pendingFileTransfer?.sourceRunId);
870
- return {
871
- verifiedFileTransferCount: evidence.length,
872
- output: `已发送 ${evidence.length} 个文件,请查收。`,
873
- };
874
- }
875
- const outcome = params.sourceRunId
876
- ? collectHermesFileTransferOutcome(messages, params.sourceRunId)
877
- : undefined;
878
- if (outcome?.kind === "clarification") {
879
- if (params.sessionId && params.sourceRunId) {
880
- await recordPendingHermesFileTransfer({
881
- gatewayId: params.gatewayId,
882
- sessionKey: params.sessionKey,
883
- hermesSessionId: params.sessionId,
884
- sourceRunId: params.sourceRunId,
885
- continuation: false,
886
- });
887
- }
888
- // Only the typed content field is displayable. Raw assistant prose is
889
- // deliberately discarded, and this outcome is never delivery evidence.
890
- return {
891
- output: outcome.assistantText ?? HERMES_FILE_TRANSFER_CONTENT_MISSING_MESSAGE,
892
- };
893
- }
894
- if (outcome?.kind === "cancelled") {
895
- await clearPendingHermesFileTransfer(params.gatewayId, params.sessionKey, params.plan.pendingFileTransfer?.sourceRunId);
896
- return {
897
- output: outcome.assistantText ?? HERMES_FILE_TRANSFER_CANCELLED_MESSAGE,
898
- };
899
- }
900
- if (outcome?.kind === "ordinary") {
901
- await clearPendingHermesFileTransfer(params.gatewayId, params.sessionKey, params.plan.pendingFileTransfer?.sourceRunId);
902
- // Ordinary chat is shown only through the typed assistantText field. The
903
- // model's ordinary label is routing metadata, never proof of delivery.
904
- return {
905
- output: outcome.assistantText ?? HERMES_FILE_TRANSFER_CONTENT_MISSING_MESSAGE,
906
- };
907
- }
908
- if (outcome?.kind === "attempted") {
909
- await clearPendingHermesFileTransfer(params.gatewayId, params.sessionKey, params.plan.pendingFileTransfer?.sourceRunId);
910
- return {
911
- verifiedFileTransferCount: 0,
912
- output: HERMES_FILE_TRANSFER_NOT_SENT_MESSAGE,
913
- };
914
- }
915
- if (params.sourceRunId && hasHermesSendFileAttempt(messages, params.sourceRunId)) {
916
- return {
917
- verifiedFileTransferCount: 0,
918
- output: HERMES_FILE_TRANSFER_NOT_SENT_MESSAGE,
919
- };
920
- }
921
- // File-transfer capability is available on every mobile turn, but capability
922
- // alone does not turn ordinary chat into a file operation. Preserve the Host
923
- // answer unless this exact turn emitted a typed outcome or actually invoked
924
- // the first-party terminal command. This keeps failure closed around a real
925
- // send attempt without replacing unrelated Hermes replies.
926
- return {};
927
- }
928
- function hasHermesSendFileAttempt(messages, sourceRunId) {
929
- const currentIndex = currentHermesTurnIndex(messages, sourceRunId);
930
- if (currentIndex < 0 || !messages)
931
- return false;
932
- for (const message of messages.slice(currentIndex + 1)) {
933
- if (normalizeHermesRole(message.role) === "user")
934
- break;
935
- if (normalizeHermesRole(message.role) !== "assistant")
936
- continue;
937
- const rawToolCalls = typeof message.tool_calls === "string"
938
- ? parseJsonValue(message.tool_calls)
939
- : message.tool_calls;
940
- if (!Array.isArray(rawToolCalls))
941
- continue;
942
- for (const rawToolCall of rawToolCalls) {
943
- const toolCall = rawToolCall && typeof rawToolCall === "object" && !Array.isArray(rawToolCall)
944
- ? rawToolCall
945
- : undefined;
946
- const fn = toolCall?.function && typeof toolCall.function === "object" && !Array.isArray(toolCall.function)
947
- ? toolCall.function
948
- : undefined;
949
- const functionName = normalizeHermesRole(fn?.name);
950
- if (functionName === "clawconnect_send_file")
951
- return true;
952
- if (functionName !== "terminal")
953
- continue;
954
- const rawArguments = typeof fn?.arguments === "string"
955
- ? parseJsonValue(fn.arguments)
956
- : fn?.arguments;
957
- const args = rawArguments && typeof rawArguments === "object" && !Array.isArray(rawArguments)
958
- ? rawArguments
959
- : undefined;
960
- if (typeof args?.command === "string" && isClawConnectSendFileCommand(args.command)) {
961
- return true;
962
- }
963
- }
964
- }
965
- return false;
966
- }
967
- function isClawConnectSendFileCommand(command) {
968
- return /(?:^|[\s;&|])(?:["'][^"']*clawconnect["']|[^\s;&|]*clawconnect)\s+send-file(?:\s|$)/i.test(command);
969
- }
970
- function parseJsonValue(value) {
971
- try {
972
- return JSON.parse(value);
973
- }
974
- catch {
975
- return undefined;
976
- }
977
- }
978
- /** Read only first-party terminal outcome records for the exact current turn. */
979
- export function collectHermesFileTransferOutcome(messages, sourceRunId) {
980
- const currentIndex = currentHermesTurnIndex(messages, sourceRunId);
981
- if (currentIndex < 0 || !messages)
982
- return undefined;
983
- const outcomes = [];
984
- for (const message of messages.slice(currentIndex + 1)) {
985
- if (normalizeHermesRole(message.role) === "user")
986
- break;
987
- if (normalizeHermesRole(message.role) !== "tool" || normalizeHermesRole(message.tool_name) !== "terminal")
988
- continue;
989
- for (const record of terminalResultRecords(message)) {
990
- if (record.protocol !== "clawconnect.hermes-file-transfer-outcome.v1"
991
- || record.sourceRunId !== sourceRunId
992
- || record.sourceRole !== "assistant"
993
- || record.status !== "completed"
994
- || record.exit_code !== 0
995
- || (record.kind !== "ordinary" && record.kind !== "clarification" && record.kind !== "attempted" && record.kind !== "cancelled"))
996
- continue;
997
- const assistantText = typeof record.assistantText === "string" ? record.assistantText.trim() : "";
998
- if (record.kind !== "attempted" && !assistantText)
999
- continue;
1000
- outcomes.push({
1001
- protocol: "clawconnect.hermes-file-transfer-outcome.v1",
1002
- kind: record.kind,
1003
- sourceRunId,
1004
- sourceRole: "assistant",
1005
- status: "completed",
1006
- ...(assistantText ? { assistantText } : {}),
1007
- });
1008
- }
1009
- }
1010
- if (outcomes.length !== 1)
1011
- return undefined;
1012
- return outcomes[0];
1013
- }
1014
- export function collectHermesFileTransferEvidence(messages, sourceRunId) {
1015
- if (!messages) {
1016
- return [];
1017
- }
1018
- const currentIndex = currentHermesTurnIndex(messages, sourceRunId);
1019
- if (currentIndex < 0) {
1020
- return [];
1021
- }
1022
- const ids = new Set();
1023
- for (const message of messages.slice(currentIndex + 1)) {
1024
- if (normalizeHermesRole(message.role) === "user") {
1025
- break;
1026
- }
1027
- if (normalizeHermesRole(message.role) !== "tool")
1028
- continue;
1029
- const toolName = normalizeHermesRole(message.tool_name);
1030
- if (toolName === "clawconnect_send_file") {
1031
- const receipt = parseJsonRecord(stringValue(message.content) ?? "");
1032
- if (receipt?.ok === true
1033
- && receipt.sourceRunId === sourceRunId
1034
- && receipt.status === "completed"
1035
- && typeof receipt.fileId === "string"
1036
- && /^file_[a-z0-9]+$/i.test(receipt.fileId)) {
1037
- ids.add(receipt.fileId);
1038
- }
1039
- continue;
1040
- }
1041
- // Only the legacy first-party terminal result is additional evidence. A
1042
- // delegate summary, wrapper output, or assistant claim cannot manufacture
1043
- // an attachment by repeating the command name and a success word.
1044
- if (toolName !== "terminal")
1045
- continue;
1046
- for (const record of terminalResultRecords(message)) {
1047
- if (record.sourceRunId !== sourceRunId
1048
- || record.sourceRole !== "assistant"
1049
- || record.status !== "completed"
1050
- || record.exit_code !== 0
1051
- || typeof record.fileId !== "string"
1052
- || !/^file_[a-z0-9]+$/i.test(record.fileId)) {
1053
- continue;
1054
- }
1055
- ids.add(record.fileId);
1056
- }
1057
- }
1058
- return [...ids];
1059
- }
1060
- function currentHermesTurnIndex(messages, sourceRunId) {
1061
- if (!messages || !sourceRunId.trim())
1062
- return -1;
1063
- let currentIndex = -1;
1064
- for (let index = 0; index < messages.length; index += 1) {
1065
- const message = messages[index];
1066
- if (normalizeHermesRole(message?.role) === "user" && hermesSourceRunIdFromMessage(message) === sourceRunId) {
1067
- currentIndex = index;
1068
- }
1069
- }
1070
- return currentIndex;
1071
- }
1072
- function terminalResultRecords(message) {
1073
- const content = stringValue(message.content);
1074
- if (!content) {
1075
- return [];
1076
- }
1077
- const outer = parseJsonRecord(content);
1078
- if (!outer) {
1079
- return [];
1080
- }
1081
- const records = [outer];
1082
- const output = stringValue(outer.output);
1083
- if (output) {
1084
- for (const fragment of extractJsonObjects(output)) {
1085
- const record = parseJsonRecord(fragment);
1086
- if (record) {
1087
- // Hermes stores the command envelope's exit_code beside the textual
1088
- // send-file JSON payload; the payload itself carries the Relay fields.
1089
- records.push(record.exit_code === undefined && outer.exit_code !== undefined
1090
- ? { ...record, exit_code: outer.exit_code }
1091
- : record);
1092
- }
1093
- }
1094
- }
1095
- return records;
1096
- }
1097
- function parseJsonRecord(value) {
1098
- try {
1099
- const parsed = JSON.parse(value);
1100
- return parsed && typeof parsed === "object" && !Array.isArray(parsed)
1101
- ? parsed
1102
- : undefined;
1103
- }
1104
- catch {
1105
- return undefined;
1106
- }
1107
- }
1108
- function extractJsonObjects(value) {
1109
- const fragments = [];
1110
- let start = -1;
1111
- let depth = 0;
1112
- let inString = false;
1113
- let escaped = false;
1114
- for (let index = 0; index < value.length; index += 1) {
1115
- const character = value[index];
1116
- if (inString) {
1117
- if (escaped) {
1118
- escaped = false;
1119
- }
1120
- else if (character === "\\") {
1121
- escaped = true;
1122
- }
1123
- else if (character === '"') {
1124
- inString = false;
1125
- }
1126
- continue;
1127
- }
1128
- if (character === '"') {
1129
- inString = true;
1130
- }
1131
- else if (character === "{") {
1132
- if (depth === 0) {
1133
- start = index;
1134
- }
1135
- depth += 1;
1136
- }
1137
- else if (character === "}" && depth > 0) {
1138
- depth -= 1;
1139
- if (depth === 0 && start >= 0) {
1140
- fragments.push(value.slice(start, index + 1));
1141
- start = -1;
1142
- }
1143
- }
1144
- }
1145
- return fragments;
1146
- }
1147
- export function buildHermesRuntimeContextHint(snapshot) {
1148
- const details = compactStringArray([
1149
- snapshot.currentModel ? `model=${snapshot.currentModel}` : undefined,
1150
- snapshot.provider ? `provider=${snapshot.provider}` : undefined,
1151
- ]);
1152
- if (details.length === 0) {
1153
- return undefined;
1154
- }
1155
- return [
1156
- "[Hermes runtime context]",
1157
- `Current runtime: ${details.join(", ")}.`,
1158
- "If the user asks which model or provider is currently being used, answer from this runtime context.",
1159
- ].join("\n");
1160
- }
1161
300
  //# sourceMappingURL=hermes-runtime-chat.js.map