clawconnect-agent 0.2.13 → 0.2.15

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