clawconnect-agent 0.2.6 → 0.2.7

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 (118) hide show
  1. package/README.md +7 -3
  2. package/README.zh-CN.md +7 -3
  3. package/dist/commands/hermes-file-transfer-outcome.d.ts +18 -0
  4. package/dist/commands/hermes-file-transfer-outcome.js +40 -0
  5. package/dist/commands/hermes-file-transfer-outcome.js.map +1 -0
  6. package/dist/commands/send-file.js +39 -33
  7. package/dist/commands/send-file.js.map +1 -1
  8. package/dist/core/command-types.d.ts +5 -0
  9. package/dist/core/relay/chat-payload.js +32 -22
  10. package/dist/core/relay/chat-payload.js.map +1 -1
  11. package/dist/core/relay/file-upload.d.ts +1 -0
  12. package/dist/core/relay/file-upload.js +3 -0
  13. package/dist/core/relay/file-upload.js.map +1 -1
  14. package/dist/core/relay/mobile-chat-run-bridge.d.ts +53 -8
  15. package/dist/core/relay/mobile-chat-run-bridge.js +45 -0
  16. package/dist/core/relay/mobile-chat-run-bridge.js.map +1 -1
  17. package/dist/core/relay/office-payload.js +54 -22
  18. package/dist/core/relay/office-payload.js.map +1 -1
  19. package/dist/core/relay/source-commit.d.ts +41 -0
  20. package/dist/core/relay/source-commit.js +118 -0
  21. package/dist/core/relay/source-commit.js.map +1 -0
  22. package/dist/core/relay/timeline-event-builder.d.ts +6 -0
  23. package/dist/core/relay/timeline-event-builder.js +3 -0
  24. package/dist/core/relay/timeline-event-builder.js.map +1 -1
  25. package/dist/core/relay/timeline-event-log.d.ts +29 -0
  26. package/dist/core/relay/timeline-event-log.js +37 -0
  27. package/dist/core/relay/timeline-event-log.js.map +1 -1
  28. package/dist/core/relay/timeline-projection-v3.d.ts +63 -0
  29. package/dist/core/relay/timeline-projection-v3.js +87 -0
  30. package/dist/core/relay/timeline-projection-v3.js.map +1 -0
  31. package/dist/hermes/hermes-relay-manager.js +3 -0
  32. package/dist/hermes/hermes-relay-manager.js.map +1 -1
  33. package/dist/hermes/hermes-runtime-test-support.d.ts +3 -3
  34. package/dist/hermes/hermes-runtime-test-support.js +12 -6
  35. package/dist/hermes/hermes-runtime-test-support.js.map +1 -1
  36. package/dist/hermes/runtime/hermes-file-transfer-state.d.ts +33 -0
  37. package/dist/hermes/runtime/hermes-file-transfer-state.js +139 -0
  38. package/dist/hermes/runtime/hermes-file-transfer-state.js.map +1 -0
  39. package/dist/hermes/runtime/hermes-mobile-file-route-store.d.ts +17 -0
  40. package/dist/hermes/runtime/hermes-mobile-file-route-store.js +103 -0
  41. package/dist/hermes/runtime/hermes-mobile-file-route-store.js.map +1 -0
  42. package/dist/hermes/runtime/hermes-runtime-api-client.d.ts +1 -0
  43. package/dist/hermes/runtime/hermes-runtime-api-client.js +16 -0
  44. package/dist/hermes/runtime/hermes-runtime-api-client.js.map +1 -1
  45. package/dist/hermes/runtime/hermes-runtime-chat.d.ts +35 -4
  46. package/dist/hermes/runtime/hermes-runtime-chat.js +426 -51
  47. package/dist/hermes/runtime/hermes-runtime-chat.js.map +1 -1
  48. package/dist/hermes/runtime/hermes-runtime-command-router.js +9 -1
  49. package/dist/hermes/runtime/hermes-runtime-command-router.js.map +1 -1
  50. package/dist/hermes/runtime/hermes-runtime-cron.d.ts +1 -0
  51. package/dist/hermes/runtime/hermes-runtime-cron.js +34 -66
  52. package/dist/hermes/runtime/hermes-runtime-cron.js.map +1 -1
  53. package/dist/hermes/runtime/hermes-runtime-history-completion.d.ts +8 -1
  54. package/dist/hermes/runtime/hermes-runtime-history-completion.js +112 -41
  55. package/dist/hermes/runtime/hermes-runtime-history-completion.js.map +1 -1
  56. package/dist/hermes/runtime/hermes-runtime-history.js +92 -17
  57. package/dist/hermes/runtime/hermes-runtime-history.js.map +1 -1
  58. package/dist/hermes/runtime/hermes-runtime-models.js +4 -1
  59. package/dist/hermes/runtime/hermes-runtime-models.js.map +1 -1
  60. package/dist/hermes/runtime/hermes-runtime-process.js +4 -3
  61. package/dist/hermes/runtime/hermes-runtime-process.js.map +1 -1
  62. package/dist/hermes/runtime/hermes-runtime-state-db.d.ts +6 -0
  63. package/dist/hermes/runtime/hermes-runtime-state-db.js +20 -1
  64. package/dist/hermes/runtime/hermes-runtime-state-db.js.map +1 -1
  65. package/dist/hermes/runtime/hermes-runtime-tool-log-watcher.js +12 -20
  66. package/dist/hermes/runtime/hermes-runtime-tool-log-watcher.js.map +1 -1
  67. package/dist/index.js +16 -2
  68. package/dist/index.js.map +1 -1
  69. package/dist/openclaw/config/provider-config.js +4 -1
  70. package/dist/openclaw/config/provider-config.js.map +1 -1
  71. package/dist/openclaw/relay/agent-identity-helpers.d.ts +9 -0
  72. package/dist/openclaw/relay/agent-identity-helpers.js +118 -0
  73. package/dist/openclaw/relay/agent-identity-helpers.js.map +1 -0
  74. package/dist/openclaw/relay/assistant-media-sidecar.d.ts +19 -2
  75. package/dist/openclaw/relay/assistant-media-sidecar.js +131 -15
  76. package/dist/openclaw/relay/assistant-media-sidecar.js.map +1 -1
  77. package/dist/openclaw/relay/chat-history.d.ts +37 -2
  78. package/dist/openclaw/relay/chat-history.js +565 -51
  79. package/dist/openclaw/relay/chat-history.js.map +1 -1
  80. package/dist/openclaw/relay/chat-run-identity.js +3 -1
  81. package/dist/openclaw/relay/chat-run-identity.js.map +1 -1
  82. package/dist/openclaw/relay/gateway-history-projection.d.ts +4 -0
  83. package/dist/openclaw/relay/gateway-history-projection.js +67 -0
  84. package/dist/openclaw/relay/gateway-history-projection.js.map +1 -0
  85. package/dist/openclaw/relay/openclaw-active-run-state.d.ts +67 -0
  86. package/dist/openclaw/relay/openclaw-active-run-state.js +283 -0
  87. package/dist/openclaw/relay/openclaw-active-run-state.js.map +1 -0
  88. package/dist/openclaw/relay/openclaw-message-tool-real-shape.fixture.d.ts +12 -0
  89. package/dist/openclaw/relay/openclaw-message-tool-real-shape.fixture.js +48 -0
  90. package/dist/openclaw/relay/openclaw-message-tool-real-shape.fixture.js.map +1 -0
  91. package/dist/openclaw/relay/openclaw-source-commit-watcher.d.ts +22 -0
  92. package/dist/openclaw/relay/openclaw-source-commit-watcher.js +64 -0
  93. package/dist/openclaw/relay/openclaw-source-commit-watcher.js.map +1 -0
  94. package/dist/openclaw/relay/outgoing-media-relay.d.ts +30 -3
  95. package/dist/openclaw/relay/outgoing-media-relay.js +407 -118
  96. package/dist/openclaw/relay/outgoing-media-relay.js.map +1 -1
  97. package/dist/openclaw/relay/relay-manager-history-timing.d.ts +1 -4
  98. package/dist/openclaw/relay/relay-manager-history-timing.js +4 -4
  99. package/dist/openclaw/relay/relay-manager-history-timing.js.map +1 -1
  100. package/dist/openclaw/relay/relay-manager-payload-helpers.d.ts +28 -1
  101. package/dist/openclaw/relay/relay-manager-payload-helpers.js +219 -11
  102. package/dist/openclaw/relay/relay-manager-payload-helpers.js.map +1 -1
  103. package/dist/openclaw/relay/session-context.d.ts +15 -0
  104. package/dist/openclaw/relay/session-context.js +48 -4
  105. package/dist/openclaw/relay/session-context.js.map +1 -1
  106. package/dist/openclaw/relay/source-commit-projection.d.ts +25 -0
  107. package/dist/openclaw/relay/source-commit-projection.js +140 -0
  108. package/dist/openclaw/relay/source-commit-projection.js.map +1 -0
  109. package/dist/openclaw/relay/transcript-dedupe.js +11 -42
  110. package/dist/openclaw/relay/transcript-dedupe.js.map +1 -1
  111. package/dist/openclaw/relay-manager.js +323 -135
  112. package/dist/openclaw/relay-manager.js.map +1 -1
  113. package/dist/openclaw/runtime/local-runtime.js +4 -2
  114. package/dist/openclaw/runtime/local-runtime.js.map +1 -1
  115. package/package.json +1 -1
  116. package/dist/openclaw/session-store.d.ts +0 -6
  117. package/dist/openclaw/session-store.js +0 -231
  118. package/dist/openclaw/session-store.js.map +0 -1
@@ -7,13 +7,15 @@ import { buildToolInvocationUpdatedEvent, } from "../../core/relay/timeline-even
7
7
  import { forgetHermesSession, getMappedHermesSessionId, rememberHermesSession, } from "../hermes-session-store.js";
8
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
9
  import { collectHermesUsageSnapshot, listHermesSessions, readCachedHermesStatusSnapshot, readHermesStatusSnapshotAsync, } from "./hermes-runtime-usage.js";
10
- import { compactStringArray, sanitizeFileName } from "./hermes-runtime-values.js";
11
- import { detectHermesHistoryCompletion, selectHermesSessionForCompletedChat, } from "./hermes-runtime-history-completion.js";
12
- import { captureHermesSessionActiveHead, rewindHermesSessionAfterActiveHead, } from "./hermes-runtime-state-db.js";
13
- import { tryRunHermesApiChat } from "./hermes-runtime-api-client.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";
13
+ import { ensureHermesApiSessionForMobileRoute, tryRunHermesApiChat } from "./hermes-runtime-api-client.js";
14
14
  import { resolveHermesPreloadedSkillContext } from "./hermes-runtime-preloaded-skills.js";
15
15
  import { createHermesToolLogWatcher, hermesToolState, } from "./hermes-runtime-tool-log-watcher.js";
16
16
  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";
17
19
  export { selectHermesSessionForCompletedChat } from "./hermes-runtime-history-completion.js";
18
20
  export { latestTerminalAssistantReplyFromHermesExport } from "./hermes-runtime-history-completion.js";
19
21
  export { parseHermesToolLogLine } from "./hermes-runtime-tool-log-watcher.js";
@@ -33,6 +35,9 @@ const EMPTY_PRELOADED_SKILL_CONTEXT = {
33
35
  requiredToolsets: [],
34
36
  skillNames: [],
35
37
  };
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 = "文件发送已取消。";
36
41
  export async function runHermesChat(params, context = {}) {
37
42
  const record = params && typeof params === "object" && !Array.isArray(params)
38
43
  ? params
@@ -56,8 +61,7 @@ export async function runHermesChat(params, context = {}) {
56
61
  const sourceRunId = typeof context.requestId === "string" && context.requestId.trim().length > 0
57
62
  ? context.requestId.trim()
58
63
  : undefined;
59
- const preparationPlan = planHermesChatPreparation(rawMessage);
60
- const preparedMessage = await prepareHermesMessage(rawMessage, record.attachments, sessionKey, sourceRunId, preparationPlan);
64
+ const preparedMessage = await prepareHermesMessage(rawMessage, record.attachments, sessionKey, sourceRunId);
61
65
  if (!preparedMessage.cliMessage.trim()) {
62
66
  throw new Error("message_required");
63
67
  }
@@ -69,7 +73,6 @@ export async function runHermesChat(params, context = {}) {
69
73
  sourceRunId,
70
74
  hermesSessionId: record.hermesSessionId,
71
75
  context,
72
- preparationPlan,
73
76
  });
74
77
  });
75
78
  }
@@ -94,30 +97,66 @@ async function runHermesChatPrepared(params) {
94
97
  : undefined;
95
98
  const mappedResume = explicitResume ? undefined : await getMappedHermesSessionId(params.sessionKey);
96
99
  let resume = explicitResume ?? mappedResume;
97
- const preloadedSkillContext = params.preparationPlan.preloadFileTransferSkill
100
+ const preparationPlan = await planHermesChatPreparation({
101
+ message: params.rawMessage,
102
+ gatewayId: params.context.gatewayId ?? "clawconnect",
103
+ sessionKey: params.sessionKey,
104
+ sessionId: resume,
105
+ sourceRunId: params.sourceRunId,
106
+ fileTransferCapability: params.context.hermesFileTransferCapability,
107
+ });
108
+ const preloadedSkillContext = preparationPlan.preloadFileTransferSkill
98
109
  ? await resolveHermesPreloadedSkillContext({ forceFileTransfer: true })
99
110
  : EMPTY_PRELOADED_SKILL_CONTEXT;
111
+ if (preparationPlan.preloadFileTransferSkill && !resume && params.sourceRunId) {
112
+ const ensuredSessionId = await ensureHermesApiSessionForMobileRoute(params.sessionKey);
113
+ if (ensuredSessionId) {
114
+ resume = ensuredSessionId;
115
+ await rememberHermesSession(params.sessionKey, {
116
+ sessionKey: params.sessionKey,
117
+ hermesSessionId: ensuredSessionId,
118
+ displayName: params.sessionKey,
119
+ kind: "hermes",
120
+ });
121
+ }
122
+ }
100
123
  try {
101
- const apiChat = await tryRunHermesApiChat({
102
- message: params.preparedMessage.apiMessage,
103
- instructions: params.preparedMessage.apiInstructions,
104
- sessionKey: params.sessionKey,
105
- resume,
106
- preloadedSkillNames: preloadedSkillContext.skillNames,
107
- requiredToolsets: preloadedSkillContext.requiredToolsets,
108
- context: params.context,
109
- });
124
+ // File delivery is fail-closed and must be backed by the local
125
+ // clawconnect send-file tool. The API stream publishes assistant deltas
126
+ // before its final tool evidence is persisted, so route these turns
127
+ // through the CLI path where the answer is buffered until verification.
128
+ const apiChat = preparationPlan.preloadFileTransferSkill
129
+ ? undefined
130
+ : await tryRunHermesApiChat({
131
+ message: params.preparedMessage.apiMessage,
132
+ instructions: params.preparedMessage.apiInstructions,
133
+ sessionKey: params.sessionKey,
134
+ resume,
135
+ preloadedSkillNames: preloadedSkillContext.skillNames,
136
+ requiredToolsets: preloadedSkillContext.requiredToolsets,
137
+ context: params.context,
138
+ });
110
139
  if (apiChat) {
111
140
  const recoveredOutput = await recoverEmptyHermesApiOutputFromHistory({
112
141
  output: apiChat.output,
113
142
  hermesSessionId: apiChat.hermesSessionId,
114
143
  sessionKey: params.sessionKey,
115
144
  userMessage: params.rawMessage,
145
+ sourceRunId: params.sourceRunId,
116
146
  abortSignal: params.context.abortSignal,
117
147
  });
148
+ const visibleOutput = requireVisibleHermesOutput(recoveredOutput || apiChat.output);
118
149
  return {
119
150
  ...apiChat,
120
- output: requireVisibleHermesOutput(recoveredOutput || apiChat.output),
151
+ output: visibleOutput,
152
+ ...(await verifyHermesFileTransferIfRequired({
153
+ plan: preparationPlan,
154
+ gatewayId: params.context.gatewayId ?? "clawconnect",
155
+ sessionKey: params.sessionKey,
156
+ sessionId: apiChat.hermesSessionId,
157
+ sourceRunId: params.sourceRunId,
158
+ output: visibleOutput,
159
+ })),
121
160
  };
122
161
  }
123
162
  }
@@ -132,25 +171,37 @@ async function runHermesChatPrepared(params) {
132
171
  }
133
172
  await forgetHermesSession(params.sessionKey, mappedResume);
134
173
  resume = undefined;
135
- const retryApiChat = await tryRunHermesApiChat({
136
- message: params.preparedMessage.apiMessage,
137
- instructions: params.preparedMessage.apiInstructions,
138
- sessionKey: params.sessionKey,
139
- preloadedSkillNames: preloadedSkillContext.skillNames,
140
- requiredToolsets: preloadedSkillContext.requiredToolsets,
141
- context: params.context,
142
- });
174
+ const retryApiChat = preparationPlan.preloadFileTransferSkill
175
+ ? undefined
176
+ : await tryRunHermesApiChat({
177
+ message: params.preparedMessage.apiMessage,
178
+ instructions: params.preparedMessage.apiInstructions,
179
+ sessionKey: params.sessionKey,
180
+ preloadedSkillNames: preloadedSkillContext.skillNames,
181
+ requiredToolsets: preloadedSkillContext.requiredToolsets,
182
+ context: params.context,
183
+ });
143
184
  if (retryApiChat) {
144
185
  const recoveredOutput = await recoverEmptyHermesApiOutputFromHistory({
145
186
  output: retryApiChat.output,
146
187
  hermesSessionId: retryApiChat.hermesSessionId,
147
188
  sessionKey: params.sessionKey,
148
189
  userMessage: params.rawMessage,
190
+ sourceRunId: params.sourceRunId,
149
191
  abortSignal: params.context.abortSignal,
150
192
  });
193
+ const visibleOutput = requireVisibleHermesOutput(recoveredOutput || retryApiChat.output);
151
194
  return {
152
195
  ...retryApiChat,
153
- output: requireVisibleHermesOutput(recoveredOutput || retryApiChat.output),
196
+ output: visibleOutput,
197
+ ...(await verifyHermesFileTransferIfRequired({
198
+ plan: preparationPlan,
199
+ gatewayId: params.context.gatewayId ?? "clawconnect",
200
+ sessionKey: params.sessionKey,
201
+ sessionId: retryApiChat.hermesSessionId,
202
+ sourceRunId: params.sourceRunId,
203
+ output: visibleOutput,
204
+ })),
154
205
  };
155
206
  }
156
207
  }
@@ -160,17 +211,21 @@ async function runHermesChatPrepared(params) {
160
211
  : undefined;
161
212
  let rawOutput;
162
213
  try {
163
- rawOutput = await runHermesChatOnce({
214
+ rawOutput = await runHermesChatOnceWithMobileFileRoute({
164
215
  message: params.preparedMessage.cliMessage,
165
216
  sessionKey: params.sessionKey,
166
217
  resume,
167
218
  context: params.context,
168
219
  preloadedSkillArgs: preloadedSkillContext.cliArgs,
220
+ enableFileTransferRoute: preparationPlan.preloadFileTransferSkill,
221
+ gatewayId: params.context.gatewayId ?? "clawconnect",
222
+ sourceRunId: params.sourceRunId,
169
223
  historyCompletion: () => detectHermesHistoryCompletion({
170
224
  beforeSessions,
171
225
  resume,
172
226
  sessionKey: params.sessionKey,
173
227
  userMessage: params.rawMessage,
228
+ sourceRunId: params.sourceRunId,
174
229
  }),
175
230
  });
176
231
  }
@@ -191,15 +246,19 @@ async function runHermesChatPrepared(params) {
191
246
  await forgetHermesSession(params.sessionKey, mappedResume);
192
247
  resume = undefined;
193
248
  cliActiveHead = undefined;
194
- rawOutput = await runHermesChatOnce({
249
+ rawOutput = await runHermesChatOnceWithMobileFileRoute({
195
250
  message: params.preparedMessage.cliMessage,
196
251
  sessionKey: params.sessionKey,
197
252
  context: params.context,
198
253
  preloadedSkillArgs: preloadedSkillContext.cliArgs,
254
+ enableFileTransferRoute: preparationPlan.preloadFileTransferSkill,
255
+ gatewayId: params.context.gatewayId ?? "clawconnect",
256
+ sourceRunId: params.sourceRunId,
199
257
  historyCompletion: () => detectHermesHistoryCompletion({
200
258
  beforeSessions,
201
259
  sessionKey: params.sessionKey,
202
260
  userMessage: params.rawMessage,
261
+ sourceRunId: params.sourceRunId,
203
262
  }),
204
263
  });
205
264
  }
@@ -214,6 +273,14 @@ async function runHermesChatPrepared(params) {
214
273
  if (mappedSession) {
215
274
  await rememberHermesSession(params.sessionKey, mappedSession);
216
275
  }
276
+ const verifiedFileTransfer = await verifyHermesFileTransferIfRequired({
277
+ plan: preparationPlan,
278
+ gatewayId: params.context.gatewayId ?? "clawconnect",
279
+ sessionKey: params.sessionKey,
280
+ sessionId: resume ?? mappedSession?.hermesSessionId,
281
+ sourceRunId: params.sourceRunId,
282
+ output,
283
+ });
217
284
  const usage = mappedSession?.hermesSessionId
218
285
  ? await collectHermesUsageSnapshot(mappedSession.hermesSessionId)
219
286
  : await readHermesStatusSnapshotAsync();
@@ -222,6 +289,7 @@ async function runHermesChatPrepared(params) {
222
289
  sessionKey: params.sessionKey,
223
290
  artifactPaths: [],
224
291
  usage,
292
+ ...verifiedFileTransfer,
225
293
  };
226
294
  }
227
295
  function isHermesChatAbortedError(error) {
@@ -289,6 +357,7 @@ async function recoverEmptyHermesApiOutputFromHistory(params) {
289
357
  resume: params.hermesSessionId,
290
358
  sessionKey: params.sessionKey,
291
359
  userMessage: params.userMessage,
360
+ sourceRunId: params.sourceRunId,
292
361
  });
293
362
  if (detectedOutput) {
294
363
  return detectedOutput;
@@ -327,6 +396,25 @@ async function runHermesChatOnce(params) {
327
396
  ? await runHermesChatStreaming(args, params.sessionKey, params.context, runId, env, params.historyCompletion)
328
397
  : runHermes(args, CHAT_TIMEOUT_MS, env);
329
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
+ }
330
418
  async function runHermesChatStreaming(args, sessionKey, context, runId, env, historyCompletion) {
331
419
  const invocation = hermesInvocation(args);
332
420
  const child = spawn(invocation.command, invocation.args, {
@@ -653,7 +741,7 @@ function buildClawConnectMobileTurnMetadata(sourceRunId, sessionKey) {
653
741
  `sessionKey: ${sessionKey}`,
654
742
  ].join("\n");
655
743
  }
656
- async function prepareHermesMessage(message, attachments, sessionKey, sourceRunId, preparationPlan = planHermesChatPreparation(message)) {
744
+ async function prepareHermesMessage(message, attachments, sessionKey, sourceRunId) {
657
745
  const refs = [];
658
746
  if (Array.isArray(attachments)) {
659
747
  await cleanupExpiredHermesInbox();
@@ -742,32 +830,319 @@ export async function cleanupExpiredHermesInbox(inboxDir = HERMES_INBOX_DIR, now
742
830
  }));
743
831
  }));
744
832
  }
745
- function planHermesChatPreparation(message) {
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;
746
846
  return {
747
- preloadFileTransferSkill: shouldPreloadHermesFileTransferSkill(message),
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 } : {}),
748
855
  };
749
856
  }
750
- function shouldPreloadHermesFileTransferSkill(message) {
751
- const text = message.trim();
752
- const lower = text.toLowerCase();
753
- if (!text) {
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)
754
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
+ }
755
1144
  }
756
- if (/\bfile-transfer\b|clawconnect\s+send-file/.test(lower)) {
757
- return true;
758
- }
759
- const pathOrExtension = /(?:^|\s|["'“”‘’])(?:~|\/|[a-z]:\\)|\.(?:png|jpe?g|gif|webp|heic|pdf|docx?|xlsx?|pptx?|zip|txt|md)\b/i.test(text);
760
- const englishSend = /\b(send|transfer|upload|attach|share|copy)\b/i.test(text);
761
- const englishFile = /\b(file|image|photo|picture|screenshot|document|attachment)\b/i.test(text) || pathOrExtension;
762
- const englishTarget = /\b(phone|iphone|mobile|clawlink|this device|me)\b/i.test(text);
763
- if (englishSend && englishFile && englishTarget) {
764
- return true;
765
- }
766
- const chineseSend = /发送|发给|发到|发过来|传到|传给|传输|上传|分享|转发|给我|发/.test(text);
767
- const chineseFile = /文件|图片|照片|截图|文档|附件|桌面|本机|路径|微信图片/.test(text) || pathOrExtension;
768
- const chineseTarget = /手机|移动端|这台|给我|发过来|iPhone|ClawLink/i.test(text);
769
- // 文件传输技能会触发 Hermes CLI 的 skills list;普通文本必须不走这条慢路径。
770
- return chineseSend && chineseFile && chineseTarget;
1145
+ return fragments;
771
1146
  }
772
1147
  export function buildHermesRuntimeContextHint(snapshot) {
773
1148
  const details = compactStringArray([