@a1hvdy/cc-openclaw 0.9.2 → 0.11.0

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 (297) hide show
  1. package/dist/src/channels/telegram/card-renderer.d.ts +63 -0
  2. package/dist/src/channels/telegram/card-renderer.js +149 -0
  3. package/dist/src/channels/telegram/card-renderer.js.map +1 -0
  4. package/dist/src/channels/telegram/completion-summary.js +20 -1
  5. package/dist/src/channels/telegram/completion-summary.js.map +1 -1
  6. package/dist/src/channels/telegram/insight-formatter.d.ts +36 -0
  7. package/dist/src/channels/telegram/insight-formatter.js +36 -0
  8. package/dist/src/channels/telegram/insight-formatter.js.map +1 -0
  9. package/dist/src/channels/telegram/live-card.d.ts +7 -90
  10. package/dist/src/channels/telegram/live-card.js +22 -265
  11. package/dist/src/channels/telegram/live-card.js.map +1 -1
  12. package/dist/src/channels/telegram/logger.d.ts +10 -0
  13. package/dist/src/channels/telegram/logger.js +13 -0
  14. package/dist/src/channels/telegram/logger.js.map +1 -0
  15. package/dist/src/channels/telegram/throttle-controller.d.ts +54 -0
  16. package/dist/src/channels/telegram/throttle-controller.js +132 -0
  17. package/dist/src/channels/telegram/throttle-controller.js.map +1 -0
  18. package/dist/src/channels/telegram/tool-tracker.js +3 -14
  19. package/dist/src/channels/telegram/tool-tracker.js.map +1 -1
  20. package/dist/src/cli/doctor.d.ts +24 -0
  21. package/dist/src/cli/doctor.js +194 -0
  22. package/dist/src/cli/doctor.js.map +1 -0
  23. package/dist/src/cli/index.d.ts +8 -0
  24. package/dist/src/cli/index.js +39 -0
  25. package/dist/src/cli/index.js.map +1 -0
  26. package/dist/src/command-router/cc-handler.js +35 -709
  27. package/dist/src/command-router/cc-handler.js.map +1 -1
  28. package/dist/src/command-router/launch-policy.d.ts +93 -0
  29. package/dist/src/command-router/launch-policy.js +323 -0
  30. package/dist/src/command-router/launch-policy.js.map +1 -0
  31. package/dist/src/command-router/resume-policy.d.ts +18 -0
  32. package/dist/src/command-router/resume-policy.js +236 -0
  33. package/dist/src/command-router/resume-policy.js.map +1 -0
  34. package/dist/src/command-router/turn-formatter.d.ts +19 -0
  35. package/dist/src/command-router/turn-formatter.js +144 -0
  36. package/dist/src/command-router/turn-formatter.js.map +1 -0
  37. package/dist/src/constants.d.ts +33 -2
  38. package/dist/src/constants.js +33 -2
  39. package/dist/src/constants.js.map +1 -1
  40. package/dist/src/lib/config.d.ts +3 -0
  41. package/dist/src/lib/config.js +46 -0
  42. package/dist/src/lib/config.js.map +1 -1
  43. package/dist/src/lib/trajectory.d.ts +1 -1
  44. package/dist/src/lib/trajectory.js.map +1 -1
  45. package/dist/src/openai-compat/bridges/allowlist.d.ts +10 -0
  46. package/dist/src/openai-compat/bridges/allowlist.js +17 -0
  47. package/dist/src/openai-compat/bridges/allowlist.js.map +1 -0
  48. package/dist/src/openai-compat/bridges/factory.d.ts +30 -0
  49. package/dist/src/openai-compat/bridges/factory.js +84 -0
  50. package/dist/src/openai-compat/bridges/factory.js.map +1 -0
  51. package/dist/src/openai-compat/bridges/media-bridge.d.ts +34 -0
  52. package/dist/src/openai-compat/bridges/media-bridge.js +20 -0
  53. package/dist/src/openai-compat/bridges/media-bridge.js.map +1 -0
  54. package/dist/src/openai-compat/bridges/openclaw-native-tools.d.ts +61 -0
  55. package/dist/src/openai-compat/bridges/openclaw-native-tools.js +171 -0
  56. package/dist/src/openai-compat/bridges/openclaw-native-tools.js.map +1 -0
  57. package/dist/src/openai-compat/bridges/openclaw-tool-registry.d.ts +26 -0
  58. package/dist/src/openai-compat/bridges/openclaw-tool-registry.js +38 -0
  59. package/dist/src/openai-compat/bridges/openclaw-tool-registry.js.map +1 -0
  60. package/dist/src/openai-compat/bridges/tts-media-bridge.d.ts +19 -0
  61. package/dist/src/openai-compat/bridges/tts-media-bridge.js +59 -0
  62. package/dist/src/openai-compat/bridges/tts-media-bridge.js.map +1 -0
  63. package/dist/src/openai-compat/non-streaming-handler.js +52 -3
  64. package/dist/src/openai-compat/non-streaming-handler.js.map +1 -1
  65. package/dist/src/openai-compat/openai-compat.js +64 -2
  66. package/dist/src/openai-compat/openai-compat.js.map +1 -1
  67. package/dist/src/openai-compat/streaming-handler.js +107 -1
  68. package/dist/src/openai-compat/streaming-handler.js.map +1 -1
  69. package/dist/src/openai-compat/voice-recovery.d.ts +56 -0
  70. package/dist/src/openai-compat/voice-recovery.js +231 -0
  71. package/dist/src/openai-compat/voice-recovery.js.map +1 -0
  72. package/dist/src/session/session-manager.d.ts +51 -0
  73. package/dist/src/session/session-manager.js +165 -1
  74. package/dist/src/session/session-manager.js.map +1 -1
  75. package/dist/src/types/tool-bridge.d.ts +2 -1
  76. package/dist/src/types/tool-bridge.js +1 -0
  77. package/dist/src/types/tool-bridge.js.map +1 -1
  78. package/package.json +1 -1
  79. package/dist/scripts/bench/ab-harness.d.ts +0 -58
  80. package/dist/scripts/bench/ab-harness.d.ts.map +0 -1
  81. package/dist/scripts/bench/ab-harness.js +0 -78
  82. package/dist/scripts/bench/ab-harness.js.map +0 -1
  83. package/dist/src/channels/adapter.d.ts.map +0 -1
  84. package/dist/src/channels/telegram/completion-summary.d.ts.map +0 -1
  85. package/dist/src/channels/telegram/error-renderer.d.ts.map +0 -1
  86. package/dist/src/channels/telegram/event-reducer.d.ts.map +0 -1
  87. package/dist/src/channels/telegram/index.d.ts.map +0 -1
  88. package/dist/src/channels/telegram/injector.d.ts.map +0 -1
  89. package/dist/src/channels/telegram/live-card.d.ts.map +0 -1
  90. package/dist/src/channels/telegram/state-machine.d.ts.map +0 -1
  91. package/dist/src/channels/telegram/tool-tracker.d.ts.map +0 -1
  92. package/dist/src/command-router/cc-handler.d.ts.map +0 -1
  93. package/dist/src/command-router/index.d.ts.map +0 -1
  94. package/dist/src/constants.d.ts.map +0 -1
  95. package/dist/src/council/consensus.d.ts.map +0 -1
  96. package/dist/src/council/council.d.ts.map +0 -1
  97. package/dist/src/council/index.d.ts.map +0 -1
  98. package/dist/src/engines/base-oneshot-session.d.ts.map +0 -1
  99. package/dist/src/engines/index.d.ts.map +0 -1
  100. package/dist/src/engines/persistent-codex-session.d.ts.map +0 -1
  101. package/dist/src/engines/persistent-cursor-session.d.ts.map +0 -1
  102. package/dist/src/engines/persistent-custom-session.d.ts.map +0 -1
  103. package/dist/src/engines/persistent-gemini-session.d.ts.map +0 -1
  104. package/dist/src/engines/persistent-session.d.ts.map +0 -1
  105. package/dist/src/health/handler.d.ts.map +0 -1
  106. package/dist/src/health/index.d.ts.map +0 -1
  107. package/dist/src/index.d.ts.map +0 -1
  108. package/dist/src/lib/auto-recovery.d.ts.map +0 -1
  109. package/dist/src/lib/cache-parity.d.ts.map +0 -1
  110. package/dist/src/lib/circuit-breaker.d.ts.map +0 -1
  111. package/dist/src/lib/config.d.ts.map +0 -1
  112. package/dist/src/lib/debug-tap.d.ts.map +0 -1
  113. package/dist/src/lib/drift-detector.d.ts.map +0 -1
  114. package/dist/src/lib/error-formatter.d.ts.map +0 -1
  115. package/dist/src/lib/heartbeat-workaround.d.ts.map +0 -1
  116. package/dist/src/lib/index.d.ts.map +0 -1
  117. package/dist/src/lib/register-guard.d.ts.map +0 -1
  118. package/dist/src/lib/route-flag.d.ts +0 -49
  119. package/dist/src/lib/route-flag.d.ts.map +0 -1
  120. package/dist/src/lib/route-flag.js +0 -52
  121. package/dist/src/lib/route-flag.js.map +0 -1
  122. package/dist/src/lib/sysprompt-strip.d.ts.map +0 -1
  123. package/dist/src/lib/telemetry.d.ts.map +0 -1
  124. package/dist/src/lib/test-mode.d.ts.map +0 -1
  125. package/dist/src/lib/vendor-paths.d.ts.map +0 -1
  126. package/dist/src/logger.d.ts.map +0 -1
  127. package/dist/src/mcp/bridge.d.ts.map +0 -1
  128. package/dist/src/mcp/index.d.ts.map +0 -1
  129. package/dist/src/models.d.ts.map +0 -1
  130. package/dist/src/openai-compat/cli-stream-parser.d.ts.map +0 -1
  131. package/dist/src/openai-compat/index.d.ts.map +0 -1
  132. package/dist/src/openai-compat/openai-compat.d.ts.map +0 -1
  133. package/dist/src/openai-compat/skill-resolver.d.ts.map +0 -1
  134. package/dist/src/openai-compat/sse-translator.d.ts.map +0 -1
  135. package/dist/src/proxy/anthropic-adapter.d.ts.map +0 -1
  136. package/dist/src/proxy/handler.d.ts.map +0 -1
  137. package/dist/src/proxy/index.d.ts.map +0 -1
  138. package/dist/src/proxy/schema-cleaner.d.ts.map +0 -1
  139. package/dist/src/proxy/thought-cache.d.ts.map +0 -1
  140. package/dist/src/session/embedded-server.d.ts.map +0 -1
  141. package/dist/src/session/inbox-manager.d.ts.map +0 -1
  142. package/dist/src/session/index.d.ts.map +0 -1
  143. package/dist/src/session/session-manager.d.ts.map +0 -1
  144. package/dist/src/session-bootstrap/cwd-patch.d.ts.map +0 -1
  145. package/dist/src/session-bootstrap/index.d.ts.map +0 -1
  146. package/dist/src/session-bootstrap/sysprompt-strip.d.ts.map +0 -1
  147. package/dist/src/session-bootstrap/think-conflict-resolver.d.ts.map +0 -1
  148. package/dist/src/types.d.ts.map +0 -1
  149. package/dist/src/validation.d.ts.map +0 -1
  150. package/dist/tests/_helpers/subprocess-mock.d.ts +0 -35
  151. package/dist/tests/_helpers/subprocess-mock.d.ts.map +0 -1
  152. package/dist/tests/_helpers/subprocess-mock.js +0 -136
  153. package/dist/tests/_helpers/subprocess-mock.js.map +0 -1
  154. package/dist/tests/auto-recovery.test.d.ts +0 -2
  155. package/dist/tests/auto-recovery.test.d.ts.map +0 -1
  156. package/dist/tests/auto-recovery.test.js +0 -189
  157. package/dist/tests/auto-recovery.test.js.map +0 -1
  158. package/dist/tests/bench-harness.test.d.ts +0 -2
  159. package/dist/tests/bench-harness.test.d.ts.map +0 -1
  160. package/dist/tests/bench-harness.test.js +0 -21
  161. package/dist/tests/bench-harness.test.js.map +0 -1
  162. package/dist/tests/cache-parity.test.d.ts +0 -2
  163. package/dist/tests/cache-parity.test.d.ts.map +0 -1
  164. package/dist/tests/cache-parity.test.js +0 -401
  165. package/dist/tests/cache-parity.test.js.map +0 -1
  166. package/dist/tests/command-router.test.d.ts +0 -2
  167. package/dist/tests/command-router.test.d.ts.map +0 -1
  168. package/dist/tests/command-router.test.js +0 -60
  169. package/dist/tests/command-router.test.js.map +0 -1
  170. package/dist/tests/council.test.d.ts +0 -2
  171. package/dist/tests/council.test.d.ts.map +0 -1
  172. package/dist/tests/council.test.js +0 -20
  173. package/dist/tests/council.test.js.map +0 -1
  174. package/dist/tests/drift-detector.test.d.ts +0 -2
  175. package/dist/tests/drift-detector.test.d.ts.map +0 -1
  176. package/dist/tests/drift-detector.test.js +0 -268
  177. package/dist/tests/drift-detector.test.js.map +0 -1
  178. package/dist/tests/eager-bootstrap-gating.test.d.ts +0 -9
  179. package/dist/tests/eager-bootstrap-gating.test.d.ts.map +0 -1
  180. package/dist/tests/eager-bootstrap-gating.test.js +0 -97
  181. package/dist/tests/eager-bootstrap-gating.test.js.map +0 -1
  182. package/dist/tests/engines.test.d.ts +0 -2
  183. package/dist/tests/engines.test.d.ts.map +0 -1
  184. package/dist/tests/engines.test.js +0 -8
  185. package/dist/tests/engines.test.js.map +0 -1
  186. package/dist/tests/error-formatter.test.d.ts +0 -2
  187. package/dist/tests/error-formatter.test.d.ts.map +0 -1
  188. package/dist/tests/error-formatter.test.js +0 -220
  189. package/dist/tests/error-formatter.test.js.map +0 -1
  190. package/dist/tests/health.test.d.ts +0 -2
  191. package/dist/tests/health.test.d.ts.map +0 -1
  192. package/dist/tests/health.test.js +0 -110
  193. package/dist/tests/health.test.js.map +0 -1
  194. package/dist/tests/heartbeat-workaround.test.d.ts +0 -2
  195. package/dist/tests/heartbeat-workaround.test.d.ts.map +0 -1
  196. package/dist/tests/heartbeat-workaround.test.js +0 -90
  197. package/dist/tests/heartbeat-workaround.test.js.map +0 -1
  198. package/dist/tests/index.test.d.ts +0 -2
  199. package/dist/tests/index.test.d.ts.map +0 -1
  200. package/dist/tests/index.test.js +0 -7
  201. package/dist/tests/index.test.js.map +0 -1
  202. package/dist/tests/lib-sysprompt-strip.test.d.ts +0 -2
  203. package/dist/tests/lib-sysprompt-strip.test.d.ts.map +0 -1
  204. package/dist/tests/lib-sysprompt-strip.test.js +0 -145
  205. package/dist/tests/lib-sysprompt-strip.test.js.map +0 -1
  206. package/dist/tests/listener-activation.test.d.ts +0 -2
  207. package/dist/tests/listener-activation.test.d.ts.map +0 -1
  208. package/dist/tests/listener-activation.test.js +0 -87
  209. package/dist/tests/listener-activation.test.js.map +0 -1
  210. package/dist/tests/mcp-bridge.test.d.ts +0 -2
  211. package/dist/tests/mcp-bridge.test.d.ts.map +0 -1
  212. package/dist/tests/mcp-bridge.test.js +0 -137
  213. package/dist/tests/mcp-bridge.test.js.map +0 -1
  214. package/dist/tests/openai-compat.test.d.ts +0 -2
  215. package/dist/tests/openai-compat.test.d.ts.map +0 -1
  216. package/dist/tests/openai-compat.test.js +0 -8
  217. package/dist/tests/openai-compat.test.js.map +0 -1
  218. package/dist/tests/proxy-heartbeat-integration.test.d.ts +0 -15
  219. package/dist/tests/proxy-heartbeat-integration.test.d.ts.map +0 -1
  220. package/dist/tests/proxy-heartbeat-integration.test.js +0 -122
  221. package/dist/tests/proxy-heartbeat-integration.test.js.map +0 -1
  222. package/dist/tests/proxy.test.d.ts +0 -2
  223. package/dist/tests/proxy.test.d.ts.map +0 -1
  224. package/dist/tests/proxy.test.js +0 -8
  225. package/dist/tests/proxy.test.js.map +0 -1
  226. package/dist/tests/register-guard-stacking.test.d.ts +0 -2
  227. package/dist/tests/register-guard-stacking.test.d.ts.map +0 -1
  228. package/dist/tests/register-guard-stacking.test.js +0 -61
  229. package/dist/tests/register-guard-stacking.test.js.map +0 -1
  230. package/dist/tests/register-guard.test.d.ts +0 -2
  231. package/dist/tests/register-guard.test.d.ts.map +0 -1
  232. package/dist/tests/register-guard.test.js +0 -129
  233. package/dist/tests/register-guard.test.js.map +0 -1
  234. package/dist/tests/route-flag-rollback.test.d.ts +0 -2
  235. package/dist/tests/route-flag-rollback.test.d.ts.map +0 -1
  236. package/dist/tests/route-flag-rollback.test.js +0 -70
  237. package/dist/tests/route-flag-rollback.test.js.map +0 -1
  238. package/dist/tests/route-flag.test.d.ts +0 -2
  239. package/dist/tests/route-flag.test.d.ts.map +0 -1
  240. package/dist/tests/route-flag.test.js +0 -101
  241. package/dist/tests/route-flag.test.js.map +0 -1
  242. package/dist/tests/session-bootstrap.test.d.ts +0 -2
  243. package/dist/tests/session-bootstrap.test.d.ts.map +0 -1
  244. package/dist/tests/session-bootstrap.test.js +0 -183
  245. package/dist/tests/session-bootstrap.test.js.map +0 -1
  246. package/dist/tests/session.test.d.ts +0 -2
  247. package/dist/tests/session.test.d.ts.map +0 -1
  248. package/dist/tests/session.test.js +0 -17
  249. package/dist/tests/session.test.js.map +0 -1
  250. package/dist/tests/state-machine.test.d.ts +0 -2
  251. package/dist/tests/state-machine.test.d.ts.map +0 -1
  252. package/dist/tests/state-machine.test.js +0 -133
  253. package/dist/tests/state-machine.test.js.map +0 -1
  254. package/dist/tests/streaming/cli-stream-parser.test.d.ts +0 -2
  255. package/dist/tests/streaming/cli-stream-parser.test.d.ts.map +0 -1
  256. package/dist/tests/streaming/cli-stream-parser.test.js +0 -233
  257. package/dist/tests/streaming/cli-stream-parser.test.js.map +0 -1
  258. package/dist/tests/streaming/feature-flag.test.d.ts +0 -14
  259. package/dist/tests/streaming/feature-flag.test.d.ts.map +0 -1
  260. package/dist/tests/streaming/feature-flag.test.js +0 -163
  261. package/dist/tests/streaming/feature-flag.test.js.map +0 -1
  262. package/dist/tests/streaming/no-tools-prompt.test.d.ts +0 -17
  263. package/dist/tests/streaming/no-tools-prompt.test.d.ts.map +0 -1
  264. package/dist/tests/streaming/no-tools-prompt.test.js +0 -229
  265. package/dist/tests/streaming/no-tools-prompt.test.js.map +0 -1
  266. package/dist/tests/streaming/skill-plus-tools.test.d.ts +0 -14
  267. package/dist/tests/streaming/skill-plus-tools.test.d.ts.map +0 -1
  268. package/dist/tests/streaming/skill-plus-tools.test.js +0 -234
  269. package/dist/tests/streaming/skill-plus-tools.test.js.map +0 -1
  270. package/dist/tests/streaming/sse-translator.test.d.ts +0 -2
  271. package/dist/tests/streaming/sse-translator.test.d.ts.map +0 -1
  272. package/dist/tests/streaming/sse-translator.test.js +0 -227
  273. package/dist/tests/streaming/sse-translator.test.js.map +0 -1
  274. package/dist/tests/streaming/tool-result-roundtrip.test.d.ts +0 -11
  275. package/dist/tests/streaming/tool-result-roundtrip.test.d.ts.map +0 -1
  276. package/dist/tests/streaming/tool-result-roundtrip.test.js +0 -215
  277. package/dist/tests/streaming/tool-result-roundtrip.test.js.map +0 -1
  278. package/dist/tests/streaming/tool-use-translation.test.d.ts +0 -10
  279. package/dist/tests/streaming/tool-use-translation.test.d.ts.map +0 -1
  280. package/dist/tests/streaming/tool-use-translation.test.js +0 -251
  281. package/dist/tests/streaming/tool-use-translation.test.js.map +0 -1
  282. package/dist/tests/telegram-bridge.test.d.ts +0 -2
  283. package/dist/tests/telegram-bridge.test.d.ts.map +0 -1
  284. package/dist/tests/telegram-bridge.test.js +0 -17
  285. package/dist/tests/telegram-bridge.test.js.map +0 -1
  286. package/dist/tests/telegram-injector.test.d.ts +0 -2
  287. package/dist/tests/telegram-injector.test.d.ts.map +0 -1
  288. package/dist/tests/telegram-injector.test.js +0 -74
  289. package/dist/tests/telegram-injector.test.js.map +0 -1
  290. package/dist/tests/telemetry.test.d.ts +0 -2
  291. package/dist/tests/telemetry.test.d.ts.map +0 -1
  292. package/dist/tests/telemetry.test.js +0 -405
  293. package/dist/tests/telemetry.test.js.map +0 -1
  294. package/dist/tests/test-mode.test.d.ts +0 -2
  295. package/dist/tests/test-mode.test.d.ts.map +0 -1
  296. package/dist/tests/test-mode.test.js +0 -39
  297. package/dist/tests/test-mode.test.js.map +0 -1
@@ -0,0 +1,231 @@
1
+ /**
2
+ * voice-recovery — server-side bridge that ensures voice delivery via
3
+ * `[[tts:text]]...[[/tts:text]]` markers even when Savvy doesn't emit them.
4
+ *
5
+ * v0.10.3 turns cc-openclaw from a passive text proxy into an active
6
+ * voice-aware bridge for three failure modes:
7
+ *
8
+ * 1. Savvy emits `<tool_calls>` XML for a native OpenClaw voice tool
9
+ * (e.g. `message.voice`) that cc-openclaw can't execute. The XML
10
+ * lands as raw text in the SSE stream. → `translateVoiceToolCalls`
11
+ * rewrites it as `[[tts:text]]` markers.
12
+ *
13
+ * 2. User explicitly asks for voice but Savvy produces markerless text
14
+ * (the dominant failure observed in the 2026-05-11 evening test).
15
+ * → `autoWrapMissingMarkers` wraps the first sentence/paragraph
16
+ * in markers so OpenClaw's `maybeApplyTtsToPayload` triggers.
17
+ *
18
+ * 3. Hint-loss between turns. → `detectVoiceIntent` gates recovery so
19
+ * it only fires on explicit voice requests, never on accidental
20
+ * voice-keyword matches in non-voice prompts.
21
+ *
22
+ * All functions are PURE — no side effects, no IO, no logging. Tests
23
+ * exercise them directly via `tests/voice-recovery.test.ts`.
24
+ */
25
+ // ── Constants ─────────────────────────────────────────────────────────────
26
+ /** Word-boundary regex for explicit voice-delivery intent in user prompts.
27
+ * Kept TIGHT to avoid false positives: "voice" alone is too broad (could be
28
+ * asking about TTS config); "send a voice note" / "speak" / "in voice" are
29
+ * the intended triggers. */
30
+ const VOICE_INTENT_REGEX = /\b(voice\s*notes?|voice\s*messages?|send\s+(?:me\s+)?(?:a\s+)?voice|in\s+voice|speak\s+(?:it|the|me|to|aloud)|say\s+(?:it|the|me).*aloud|tell\s+me\s+(?:out\s+loud|aloud)|read\s+(?:it|the|this)\s+aloud)\b/i;
31
+ /** Markers OpenClaw's `maybeApplyTtsToPayload` looks for. */
32
+ const TTS_OPEN = '[[tts:text]]';
33
+ const TTS_CLOSE = '[[/tts:text]]';
34
+ /** Default max chars inside the spoken block. Matches the TTS_RULE budget. */
35
+ export const DEFAULT_MAX_SPOKEN_CHARS = 500;
36
+ // ── Diagnostic logging (v0.10.4) ──────────────────────────────────────────
37
+ /** v0.10.4 — env-gated debug logger for triangulating voice-recovery
38
+ * failure mode on Telegram vs. direct-probe paths. Single-line JSON for
39
+ * grep-ability via `pm2 logs openclaw-gateway | grep _voice_debug`.
40
+ * Disabled unless `CC_OPENCLAW_VOICE_DEBUG=1`. Safe to leave in code
41
+ * permanently — the gate is a single string equality check. */
42
+ export function _logVoiceDebug(label, fields) {
43
+ if (process.env.CC_OPENCLAW_VOICE_DEBUG !== '1')
44
+ return;
45
+ try {
46
+ const payload = { _voice_debug: label, ...fields, ts: new Date().toISOString() };
47
+ // eslint-disable-next-line no-console
48
+ console.error(JSON.stringify(payload));
49
+ }
50
+ catch {
51
+ // Logging must never throw into the request path
52
+ }
53
+ }
54
+ // ── Public API ────────────────────────────────────────────────────────────
55
+ /** Detect whether the user's prompt explicitly requests a voice note. */
56
+ export function detectVoiceIntent(userPrompt) {
57
+ if (!userPrompt)
58
+ return false;
59
+ return VOICE_INTENT_REGEX.test(userPrompt);
60
+ }
61
+ /** Check whether the reply text already contains a complete `[[tts:text]]`
62
+ * block (open + close pair). */
63
+ export function hasTtsMarkers(text) {
64
+ if (!text)
65
+ return false;
66
+ const openIdx = text.indexOf(TTS_OPEN);
67
+ if (openIdx === -1)
68
+ return false;
69
+ const closeIdx = text.indexOf(TTS_CLOSE, openIdx + TTS_OPEN.length);
70
+ return closeIdx !== -1;
71
+ }
72
+ /** Pattern matching `<tool_calls>` or `<tool_use>` or `<function_calls>` XML
73
+ * blocks whose `name` attribute looks like a voice/TTS delivery tool.
74
+ * Matches names: `voice`, `speak`, `tts`, `sendVoice`, `send_voice`,
75
+ * `message.voice`, `message_voice`, `messageVoice`, and `message.send` if
76
+ * it carries an `audioAsVoice` or `as_voice` argument. Permissive on
77
+ * purpose — Phase H of the ship plan analyzes the actual tool name from
78
+ * a sysprompt dump and may narrow this in v0.10.4. */
79
+ const VOICE_TOOL_NAME_REGEX = /(?:voice|speak|tts|send[\s_-]?voice|message[.\s_-]?voice|message[.\s_-]?send.*?(?:audioAsVoice|as[\s_-]?voice))/i;
80
+ /** Loose regex catching the common Anthropic-style tool-call XML envelopes. */
81
+ const TOOL_CALL_XML_BLOCK_REGEX = /<(tool_calls?|tool_use|function_calls?)\b[^>]*>([\s\S]*?)<\/\1>/gi;
82
+ /** Inside a tool-call XML block (or the full <tag name="..."> opening),
83
+ * find the `name` attribute or `<name>` field or JSON-style `"name":"..."`. */
84
+ const TOOL_NAME_REGEX = /name\s*=\s*"([^"]+)"|<name>\s*([^<]+?)\s*<\/name>|"name"\s*:\s*"([^"]+)"/i;
85
+ /** Inside a tool-call XML block, extract the spoken text argument. Tries
86
+ * several common shapes: `<text>X</text>`, `<input>X</input>`,
87
+ * `<content>X</content>`, `"text":"X"`, `"input":"X"`, `"content":"X"`.
88
+ * Returns the first non-empty match, trimmed. */
89
+ function extractSpokenText(xmlBody) {
90
+ const xmlTagPatterns = [/<text>([\s\S]*?)<\/text>/i, /<input>([\s\S]*?)<\/input>/i, /<content>([\s\S]*?)<\/content>/i, /<speech>([\s\S]*?)<\/speech>/i];
91
+ for (const re of xmlTagPatterns) {
92
+ const m = xmlBody.match(re);
93
+ if (m && m[1]) {
94
+ const trimmed = m[1].trim();
95
+ if (trimmed)
96
+ return trimmed;
97
+ }
98
+ }
99
+ const jsonPatterns = [/"text"\s*:\s*"((?:[^"\\]|\\.)*)"/i, /"input"\s*:\s*"((?:[^"\\]|\\.)*)"/i, /"content"\s*:\s*"((?:[^"\\]|\\.)*)"/i, /"speech"\s*:\s*"((?:[^"\\]|\\.)*)"/i];
100
+ for (const re of jsonPatterns) {
101
+ const m = xmlBody.match(re);
102
+ if (m && m[1]) {
103
+ const decoded = m[1].replace(/\\n/g, '\n').replace(/\\"/g, '"').replace(/\\\\/g, '\\').trim();
104
+ if (decoded)
105
+ return decoded;
106
+ }
107
+ }
108
+ return null;
109
+ }
110
+ /** If `text` contains a `<tool_calls>` XML block targeting a voice-delivery
111
+ * tool, extract the spoken-text argument. Returns the spoken text or null. */
112
+ export function extractTtsToolCallText(text) {
113
+ if (!text)
114
+ return null;
115
+ TOOL_CALL_XML_BLOCK_REGEX.lastIndex = 0;
116
+ let match;
117
+ while ((match = TOOL_CALL_XML_BLOCK_REGEX.exec(text)) !== null) {
118
+ const body = match[2] ?? '';
119
+ // Search the FULL match (including opening tag) so `name="..."` attributes
120
+ // on the outer <tool_use name="speak"> tag are caught. Also handles
121
+ // JSON-style `"name":"..."` payloads inside the body.
122
+ const nameMatch = match[0].match(TOOL_NAME_REGEX);
123
+ const toolName = (nameMatch?.[1] ?? nameMatch?.[2] ?? nameMatch?.[3] ?? '').trim();
124
+ if (toolName && VOICE_TOOL_NAME_REGEX.test(toolName)) {
125
+ const spoken = extractSpokenText(body);
126
+ if (spoken)
127
+ return spoken;
128
+ }
129
+ }
130
+ return null;
131
+ }
132
+ /** Rewrite `text`: if a voice-tool XML block is present, remove it and
133
+ * insert a `[[tts:text]]<spoken>[[/tts:text]]` block in its place. Idempotent
134
+ * when no voice-tool XML is present. */
135
+ export function translateVoiceToolCalls(text) {
136
+ if (!text)
137
+ return text;
138
+ TOOL_CALL_XML_BLOCK_REGEX.lastIndex = 0;
139
+ // Walk the matches, build a replacement.
140
+ let result = '';
141
+ let lastEnd = 0;
142
+ let match;
143
+ TOOL_CALL_XML_BLOCK_REGEX.lastIndex = 0;
144
+ while ((match = TOOL_CALL_XML_BLOCK_REGEX.exec(text)) !== null) {
145
+ const body = match[2] ?? '';
146
+ // Search the FULL match (including opening tag) so `name="..."` attributes
147
+ // on the outer <tool_use name="speak"> tag are caught. Also handles
148
+ // JSON-style `"name":"..."` payloads inside the body.
149
+ const nameMatch = match[0].match(TOOL_NAME_REGEX);
150
+ const toolName = (nameMatch?.[1] ?? nameMatch?.[2] ?? nameMatch?.[3] ?? '').trim();
151
+ if (toolName && VOICE_TOOL_NAME_REGEX.test(toolName)) {
152
+ const spoken = extractSpokenText(body);
153
+ if (spoken) {
154
+ result += text.slice(lastEnd, match.index);
155
+ result += `${TTS_OPEN}${spoken}${TTS_CLOSE}`;
156
+ lastEnd = match.index + match[0].length;
157
+ }
158
+ }
159
+ }
160
+ result += text.slice(lastEnd);
161
+ return result;
162
+ }
163
+ /** Truncate at a sentence boundary if possible, otherwise hard-cut.
164
+ * Used to keep the spoken portion within the voice-budget. */
165
+ function truncateAtSentence(text, maxChars) {
166
+ if (text.length <= maxChars)
167
+ return text;
168
+ const slice = text.slice(0, maxChars);
169
+ // Try last sentence-ending punctuation
170
+ const lastSentence = Math.max(slice.lastIndexOf('. '), slice.lastIndexOf('! '), slice.lastIndexOf('? '));
171
+ if (lastSentence > maxChars * 0.5)
172
+ return slice.slice(0, lastSentence + 1);
173
+ // Otherwise hard-cut at last space
174
+ const lastSpace = slice.lastIndexOf(' ');
175
+ if (lastSpace > maxChars * 0.5)
176
+ return slice.slice(0, lastSpace) + '…';
177
+ return slice + '…';
178
+ }
179
+ /** If `text` is missing voice markers, find a reasonable spoken summary
180
+ * (first paragraph up to `maxSpokenChars`) and wrap it. Returns text with
181
+ * the marker block prepended; the original first paragraph is left in place
182
+ * as text expansion. Idempotent when markers already present. */
183
+ export function autoWrapMissingMarkers(text, maxSpokenChars = DEFAULT_MAX_SPOKEN_CHARS) {
184
+ if (!text)
185
+ return text;
186
+ if (hasTtsMarkers(text))
187
+ return text;
188
+ const trimmed = text.trim();
189
+ if (!trimmed)
190
+ return text;
191
+ // Pick the first paragraph (up to the first blank line). The ★ Insight
192
+ // block typically appears below — leave it alone.
193
+ const firstBlankLine = trimmed.search(/\n\s*\n/);
194
+ const firstPara = firstBlankLine === -1 ? trimmed : trimmed.slice(0, firstBlankLine).trim();
195
+ // Strip leading boilerplate Savvy sometimes adds ("Let me send the voice note now.")
196
+ const filtered = firstPara
197
+ .replace(/^(?:i (?:have|got) what i need\.?\s*)?let me send (?:the\s+)?voice (?:note|message)(?:\s+now)?\.?\s*/i, '')
198
+ .replace(/^(?:here(?:'s|\s+is)|on\s+it|sending\s+(?:a\s+)?voice(?:\s+note)?)[:.]?\s*/i, '')
199
+ .trim();
200
+ // Fall back to the original first paragraph if filtering removed everything
201
+ const spokenRaw = filtered || firstPara;
202
+ const spoken = truncateAtSentence(spokenRaw, maxSpokenChars);
203
+ if (!spoken)
204
+ return text;
205
+ // Prepend the marker block; keep the rest of the original text as text expansion.
206
+ // Use \n\n separator so the marker block is visually distinct.
207
+ return `${TTS_OPEN}${spoken}${TTS_CLOSE}\n\n${text}`;
208
+ }
209
+ /** Convenience helper: run the full recovery pipeline on a reply when the
210
+ * user has expressed voice intent. Returns the (possibly rewritten) text. */
211
+ export function applyVoiceRecovery(userPrompt, replyText, maxSpokenChars = DEFAULT_MAX_SPOKEN_CHARS) {
212
+ if (!detectVoiceIntent(userPrompt)) {
213
+ return { text: replyText, recovered: false, via: 'none' };
214
+ }
215
+ // First try translating any tool-call XML
216
+ const translated = translateVoiceToolCalls(replyText);
217
+ if (translated !== replyText && hasTtsMarkers(translated)) {
218
+ return { text: translated, recovered: true, via: 'tool-translate' };
219
+ }
220
+ // If markers already present, nothing to do
221
+ if (hasTtsMarkers(translated)) {
222
+ return { text: translated, recovered: false, via: 'none' };
223
+ }
224
+ // Auto-wrap the first paragraph
225
+ const wrapped = autoWrapMissingMarkers(translated, maxSpokenChars);
226
+ if (wrapped !== translated && hasTtsMarkers(wrapped)) {
227
+ return { text: wrapped, recovered: true, via: 'auto-wrap' };
228
+ }
229
+ return { text: translated, recovered: false, via: 'none' };
230
+ }
231
+ //# sourceMappingURL=voice-recovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voice-recovery.js","sourceRoot":"","sources":["../../../src/openai-compat/voice-recovery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,6EAA6E;AAE7E;;;6BAG6B;AAC7B,MAAM,kBAAkB,GACtB,8MAA8M,CAAC;AAEjN,6DAA6D;AAC7D,MAAM,QAAQ,GAAG,cAAc,CAAC;AAChC,MAAM,SAAS,GAAG,eAAe,CAAC;AAElC,8EAA8E;AAC9E,MAAM,CAAC,MAAM,wBAAwB,GAAG,GAAG,CAAC;AAE5C,6EAA6E;AAE7E;;;;gEAIgE;AAChE,MAAM,UAAU,cAAc,CAAC,KAAa,EAAE,MAA+B;IAC3E,IAAI,OAAO,CAAC,GAAG,CAAC,uBAAuB,KAAK,GAAG;QAAE,OAAO;IACxD,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,EAAE,YAAY,EAAE,KAAK,EAAE,GAAG,MAAM,EAAE,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC;QACjF,sCAAsC;QACtC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,iDAAiD;IACnD,CAAC;AACH,CAAC;AAED,6EAA6E;AAE7E,yEAAyE;AACzE,MAAM,UAAU,iBAAiB,CAAC,UAAkB;IAClD,IAAI,CAAC,UAAU;QAAE,OAAO,KAAK,CAAC;IAC9B,OAAO,kBAAkB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;AAC7C,CAAC;AAED;iCACiC;AACjC,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC;IACxB,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACvC,IAAI,OAAO,KAAK,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IACjC,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;IACpE,OAAO,QAAQ,KAAK,CAAC,CAAC,CAAC;AACzB,CAAC;AAED;;;;;;uDAMuD;AACvD,MAAM,qBAAqB,GACzB,kHAAkH,CAAC;AAErH,+EAA+E;AAC/E,MAAM,yBAAyB,GAC7B,mEAAmE,CAAC;AAEtE;gFACgF;AAChF,MAAM,eAAe,GACnB,2EAA2E,CAAC;AAE9E;;;kDAGkD;AAClD,SAAS,iBAAiB,CAAC,OAAe;IACxC,MAAM,cAAc,GAAG,CAAC,2BAA2B,EAAE,6BAA6B,EAAE,iCAAiC,EAAE,+BAA+B,CAAC,CAAC;IACxJ,KAAK,MAAM,EAAE,IAAI,cAAc,EAAE,CAAC;QAChC,MAAM,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC5B,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YACd,MAAM,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YAC5B,IAAI,OAAO;gBAAE,OAAO,OAAO,CAAC;QAC9B,CAAC;IACH,CAAC;IACD,MAAM,YAAY,GAAG,CAAC,mCAAmC,EAAE,oCAAoC,EAAE,sCAAsC,EAAE,qCAAqC,CAAC,CAAC;IAChL,KAAK,MAAM,EAAE,IAAI,YAAY,EAAE,CAAC;QAC9B,MAAM,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC5B,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YACd,MAAM,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;YAC9F,IAAI,OAAO;gBAAE,OAAO,OAAO,CAAC;QAC9B,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;+EAC+E;AAC/E,MAAM,UAAU,sBAAsB,CAAC,IAAY;IACjD,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,yBAAyB,CAAC,SAAS,GAAG,CAAC,CAAC;IACxC,IAAI,KAA6B,CAAC;IAClC,OAAO,CAAC,KAAK,GAAG,yBAAyB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAC/D,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC5B,2EAA2E;QAC3E,oEAAoE;QACpE,sDAAsD;QACtD,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;QAClD,MAAM,QAAQ,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,IAAI,SAAS,EAAE,CAAC,CAAC,CAAC,IAAI,SAAS,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACnF,IAAI,QAAQ,IAAI,qBAAqB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;YACrD,MAAM,MAAM,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC;YACvC,IAAI,MAAM;gBAAE,OAAO,MAAM,CAAC;QAC5B,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;yCAEyC;AACzC,MAAM,UAAU,uBAAuB,CAAC,IAAY;IAClD,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,yBAAyB,CAAC,SAAS,GAAG,CAAC,CAAC;IACxC,yCAAyC;IACzC,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,IAAI,KAA6B,CAAC;IAClC,yBAAyB,CAAC,SAAS,GAAG,CAAC,CAAC;IACxC,OAAO,CAAC,KAAK,GAAG,yBAAyB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAC/D,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC5B,2EAA2E;QAC3E,oEAAoE;QACpE,sDAAsD;QACtD,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;QAClD,MAAM,QAAQ,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,IAAI,SAAS,EAAE,CAAC,CAAC,CAAC,IAAI,SAAS,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACnF,IAAI,QAAQ,IAAI,qBAAqB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;YACrD,MAAM,MAAM,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC;YACvC,IAAI,MAAM,EAAE,CAAC;gBACX,MAAM,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;gBAC3C,MAAM,IAAI,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,EAAE,CAAC;gBAC7C,OAAO,GAAG,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;YAC1C,CAAC;QACH,CAAC;IACH,CAAC;IACD,MAAM,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAC9B,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;+DAC+D;AAC/D,SAAS,kBAAkB,CAAC,IAAY,EAAE,QAAgB;IACxD,IAAI,IAAI,CAAC,MAAM,IAAI,QAAQ;QAAE,OAAO,IAAI,CAAC;IACzC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;IACtC,uCAAuC;IACvC,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC;IACzG,IAAI,YAAY,GAAG,QAAQ,GAAG,GAAG;QAAE,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,YAAY,GAAG,CAAC,CAAC,CAAC;IAC3E,mCAAmC;IACnC,MAAM,SAAS,GAAG,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACzC,IAAI,SAAS,GAAG,QAAQ,GAAG,GAAG;QAAE,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,GAAG,GAAG,CAAC;IACvE,OAAO,KAAK,GAAG,GAAG,CAAC;AACrB,CAAC;AAED;;;kEAGkE;AAClE,MAAM,UAAU,sBAAsB,CACpC,IAAY,EACZ,iBAAyB,wBAAwB;IAEjD,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,IAAI,aAAa,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACrC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAC5B,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAC1B,uEAAuE;IACvE,kDAAkD;IAClD,MAAM,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IACjD,MAAM,SAAS,GAAG,cAAc,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC,IAAI,EAAE,CAAC;IAC5F,qFAAqF;IACrF,MAAM,QAAQ,GAAG,SAAS;SACvB,OAAO,CAAC,uGAAuG,EAAE,EAAE,CAAC;SACpH,OAAO,CAAC,6EAA6E,EAAE,EAAE,CAAC;SAC1F,IAAI,EAAE,CAAC;IACV,4EAA4E;IAC5E,MAAM,SAAS,GAAG,QAAQ,IAAI,SAAS,CAAC;IACxC,MAAM,MAAM,GAAG,kBAAkB,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC;IAC7D,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACzB,kFAAkF;IAClF,+DAA+D;IAC/D,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,OAAO,IAAI,EAAE,CAAC;AACvD,CAAC;AAED;8EAC8E;AAC9E,MAAM,UAAU,kBAAkB,CAChC,UAAkB,EAClB,SAAiB,EACjB,iBAAyB,wBAAwB;IAEjD,IAAI,CAAC,iBAAiB,CAAC,UAAU,CAAC,EAAE,CAAC;QACnC,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC;IAC5D,CAAC;IACD,0CAA0C;IAC1C,MAAM,UAAU,GAAG,uBAAuB,CAAC,SAAS,CAAC,CAAC;IACtD,IAAI,UAAU,KAAK,SAAS,IAAI,aAAa,CAAC,UAAU,CAAC,EAAE,CAAC;QAC1D,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,EAAE,gBAAgB,EAAE,CAAC;IACtE,CAAC;IACD,4CAA4C;IAC5C,IAAI,aAAa,CAAC,UAAU,CAAC,EAAE,CAAC;QAC9B,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC;IAC7D,CAAC;IACD,gCAAgC;IAChC,MAAM,OAAO,GAAG,sBAAsB,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC;IACnE,IAAI,OAAO,KAAK,UAAU,IAAI,aAAa,CAAC,OAAO,CAAC,EAAE,CAAC;QACrD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,EAAE,WAAW,EAAE,CAAC;IAC9D,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC;AAC7D,CAAC"}
@@ -28,6 +28,8 @@ export declare class SessionManager {
28
28
  private sessions;
29
29
  private _pendingSessions;
30
30
  private cleanupTimer;
31
+ private stalledWatchTimer;
32
+ private _recentSpawns;
31
33
  private pluginConfig;
32
34
  private persistedSessions;
33
35
  private _debouncedSave;
@@ -207,5 +209,54 @@ export declare class SessionManager {
207
209
  }): UltrareviewResult;
208
210
  ultrareviewStatus(id: string): UltrareviewResult | undefined;
209
211
  private _cleanupIdleSessions;
212
+ /**
213
+ * v0.10.0 — runtime stalled-session watchdog.
214
+ *
215
+ * Fires every STALLED_WATCH_INTERVAL_MS. For each session that is
216
+ * currently `isBusy === true` (mid-turn) AND whose underlying
217
+ * PersistentClaudeSession has not received any subprocess event for
218
+ * STALLED_SESSION_KILL_MS, the watchdog:
219
+ *
220
+ * 1. Logs the stall
221
+ * 2. Emits a `session_stalled_killed` trajectory event
222
+ * 3. Calls session.stop() (SIGTERM, then SIGKILL after STOP_SIGKILL_DELAY_MS)
223
+ * 4. Removes the entry from the in-memory `sessions` Map
224
+ *
225
+ * The in-flight `sendMessage()` promise will reject with the existing
226
+ * `TURN_TIMEOUT_MS` error or a session-stop error. The outer agent-runner
227
+ * then fast-fails to the cross-engine fallback (`openai-codex/gpt-5.4`)
228
+ * rather than waiting the full provider envelope (900s).
229
+ *
230
+ * Threshold is overridable via `CC_OPENCLAW_STALLED_KILL_MS` env var.
231
+ *
232
+ * Mirrors `gateway-pm2-wrapper.sh:53-60` boot-time orphan reaper.
233
+ */
234
+ private _watchStalledSessions;
235
+ /**
236
+ * Resolve the stalled-session kill threshold. Env var
237
+ * `CC_OPENCLAW_STALLED_KILL_MS` overrides STALLED_SESSION_KILL_MS at
238
+ * runtime so the value can be tuned without rebuild.
239
+ */
240
+ private _stalledThresholdMs;
241
+ /**
242
+ * v0.10.1 — record a fresh subprocess spawn for the runaway-loop
243
+ * watchdog. Trims entries older than RUNAWAY_LOOP_WINDOW_MS so the
244
+ * array length is bounded.
245
+ */
246
+ private _recordSpawn;
247
+ /**
248
+ * v0.10.1 — true when the spawn rate over RUNAWAY_LOOP_WINDOW_MS
249
+ * exceeds the configured threshold (env-overridable). Called by
250
+ * `_doStartSession` BEFORE recording the new spawn — i.e. the check
251
+ * fires when the (N+1)-th spawn attempt would push the count over.
252
+ */
253
+ private _isRunawayLoop;
254
+ /**
255
+ * v0.10.1 — resolve the runaway-loop spawn threshold. Env var
256
+ * `CC_OPENCLAW_LOOP_MAX_SUBPROCS` overrides RUNAWAY_LOOP_MAX_SUBPROCS
257
+ * for runtime tuning without rebuild. Clamped to [2, 20] to prevent
258
+ * accidental disable (0/1) or unbounded raise.
259
+ */
260
+ private _runawayThreshold;
210
261
  }
211
262
  export {};
@@ -111,13 +111,19 @@ import { PersistentCustomSession } from '../engines/persistent-custom-session.js
111
111
  import { overrideModelPricing, } from '../types.js';
112
112
  import { resolveAlias, isClaudeModel } from '../models.js';
113
113
  import { Council } from '../council/council.js';
114
- import { PERSIST_DISK_TTL_MS, DEBOUNCED_SAVE_MS, CLEANUP_INTERVAL_MS, TURN_TIMEOUT_MS, GREP_HISTORY_FETCH, TEAM_LIST_TIMEOUT_MS, TEAM_SEND_TIMEOUT_MS, RESULT_TTL_MS, ULTRAPLAN_TIMEOUT_MS, ULTRAREVIEW_POLL_INTERVAL_MS, STOP_SIGKILL_DELAY_MS, SESSION_EVENT, DEFAULT_HISTORY_LIMIT, } from '../constants.js';
114
+ import { PERSIST_DISK_TTL_MS, DEBOUNCED_SAVE_MS, CLEANUP_INTERVAL_MS, TURN_TIMEOUT_MS, GREP_HISTORY_FETCH, TEAM_LIST_TIMEOUT_MS, TEAM_SEND_TIMEOUT_MS, RESULT_TTL_MS, ULTRAPLAN_TIMEOUT_MS, ULTRAREVIEW_POLL_INTERVAL_MS, STOP_SIGKILL_DELAY_MS, SESSION_EVENT, DEFAULT_HISTORY_LIMIT, STALLED_SESSION_KILL_MS, STALLED_WATCH_INTERVAL_MS, RUNAWAY_LOOP_MAX_SUBPROCS, RUNAWAY_LOOP_WINDOW_MS, } from '../constants.js';
115
+ import * as trajectory from '../lib/trajectory.js';
115
116
  import { getGatewayUrl, getGatewayKey, getAnthropicApiKey, getOpenaiApiKey, getGeminiApiKey, getGeminiBin, getCodexBin, getCursorBin, } from '../lib/config.js';
116
117
  // ─── SessionManager ──────────────────────────────────────────────────────────
117
118
  export class SessionManager {
118
119
  sessions = new Map();
119
120
  _pendingSessions = new Map();
120
121
  cleanupTimer = null;
122
+ stalledWatchTimer = null;
123
+ // v0.10.1: rolling spawn-timestamp log for the runaway-loop watchdog.
124
+ // Each new subprocess spawn pushes Date.now(); entries older than
125
+ // RUNAWAY_LOOP_WINDOW_MS are trimmed before each check.
126
+ _recentSpawns = [];
121
127
  pluginConfig;
122
128
  persistedSessions;
123
129
  _debouncedSave;
@@ -149,6 +155,11 @@ export class SessionManager {
149
155
  this._debouncedSave = makeDebounced(() => savePersistedSessionsAsync(this.persistedSessions, this.logger), DEBOUNCED_SAVE_MS);
150
156
  // Start TTL cleanup timer
151
157
  this.cleanupTimer = setInterval(() => this._cleanupIdleSessions(), CLEANUP_INTERVAL_MS);
158
+ // v0.10.0: Start runtime stalled-session watchdog. Mirrors the
159
+ // boot-time orphan reaper in gateway-pm2-wrapper.sh:53-60, but runs
160
+ // continuously. Catches subprocesses that hang at `model_call:started`
161
+ // without emitting any output for STALLED_SESSION_KILL_MS.
162
+ this.stalledWatchTimer = setInterval(() => this._watchStalledSessions(), STALLED_WATCH_INTERVAL_MS);
152
163
  }
153
164
  // ─── Session Lifecycle ─────────────────────────────────────────────────
154
165
  async startSession(config) {
@@ -176,6 +187,24 @@ export class SessionManager {
176
187
  if (this.sessions.size >= this.pluginConfig.maxConcurrentSessions) {
177
188
  throw new Error(`Max concurrent sessions (${this.pluginConfig.maxConcurrentSessions}) reached`);
178
189
  }
190
+ // v0.10.1: runaway-loop watchdog. Refuse the spawn if OpenClaw is
191
+ // hammering us with new-session requests faster than a real
192
+ // conversation justifies. See RUNAWAY_LOOP_MAX_SUBPROCS comment in
193
+ // constants.ts for the 2026-05-11 incident context.
194
+ if (this._isRunawayLoop()) {
195
+ const count = this._recentSpawns.length;
196
+ const thresholdMax = this._runawayThreshold();
197
+ const windowMs = RUNAWAY_LOOP_WINDOW_MS;
198
+ this.logger.warn(`[watchdog] refusing new session "${name}" — runaway loop detected (${count} spawns in last ${Math.round(windowMs / 1000)}s, threshold=${thresholdMax})`);
199
+ try {
200
+ trajectory.emit('runaway_loop_killed', { count, windowMs, thresholdMax, attemptedName: name }, name);
201
+ }
202
+ catch {
203
+ // Trajectory is observability — must never block the refusal
204
+ }
205
+ throw new Error(`cc-openclaw: runaway loop — ${count} subprocess spawns in last ${Math.round(windowMs / 1000)}s exceeds threshold ${thresholdMax}; aborting to prevent gateway thrash`);
206
+ }
207
+ this._recordSpawn();
179
208
  // Auto-resume: if we have a persisted claudeSessionId for this name, inject it.
180
209
  // Skip when config.skipPersistence is set (e.g. openai-compat bridge sessions
181
210
  // that must NOT resume stale CLI state from a previous server run).
@@ -738,6 +767,10 @@ export class SessionManager {
738
767
  clearInterval(this.cleanupTimer);
739
768
  this.cleanupTimer = null;
740
769
  }
770
+ if (this.stalledWatchTimer) {
771
+ clearInterval(this.stalledWatchTimer);
772
+ this.stalledWatchTimer = null;
773
+ }
741
774
  // Stop ultrareview pollers
742
775
  for (const [, timer] of this.ultrareviewPollers)
743
776
  clearInterval(timer);
@@ -1384,5 +1417,136 @@ export class SessionManager {
1384
1417
  if (pruned)
1385
1418
  savePersistedSessionsAsync(this.persistedSessions);
1386
1419
  }
1420
+ /**
1421
+ * v0.10.0 — runtime stalled-session watchdog.
1422
+ *
1423
+ * Fires every STALLED_WATCH_INTERVAL_MS. For each session that is
1424
+ * currently `isBusy === true` (mid-turn) AND whose underlying
1425
+ * PersistentClaudeSession has not received any subprocess event for
1426
+ * STALLED_SESSION_KILL_MS, the watchdog:
1427
+ *
1428
+ * 1. Logs the stall
1429
+ * 2. Emits a `session_stalled_killed` trajectory event
1430
+ * 3. Calls session.stop() (SIGTERM, then SIGKILL after STOP_SIGKILL_DELAY_MS)
1431
+ * 4. Removes the entry from the in-memory `sessions` Map
1432
+ *
1433
+ * The in-flight `sendMessage()` promise will reject with the existing
1434
+ * `TURN_TIMEOUT_MS` error or a session-stop error. The outer agent-runner
1435
+ * then fast-fails to the cross-engine fallback (`openai-codex/gpt-5.4`)
1436
+ * rather than waiting the full provider envelope (900s).
1437
+ *
1438
+ * Threshold is overridable via `CC_OPENCLAW_STALLED_KILL_MS` env var.
1439
+ *
1440
+ * Mirrors `gateway-pm2-wrapper.sh:53-60` boot-time orphan reaper.
1441
+ */
1442
+ _watchStalledSessions() {
1443
+ const thresholdMs = this._stalledThresholdMs();
1444
+ const now = Date.now();
1445
+ for (const [name, managed] of this.sessions) {
1446
+ // Skip sessions that aren't currently in-flight; idle TTL cleanup
1447
+ // handles those.
1448
+ if (!managed.session.isBusy)
1449
+ continue;
1450
+ // PersistentClaudeSession.stats.lastActivity is an ISO string updated
1451
+ // on every NDJSON event from the subprocess (persistent-session.ts:395).
1452
+ // If null, the session never received a single event — definitely stuck.
1453
+ const stats = managed.session.getStats();
1454
+ const lastActivityIso = stats.lastActivity;
1455
+ const lastEventMs = lastActivityIso
1456
+ ? new Date(lastActivityIso).getTime()
1457
+ : managed.lastActivity;
1458
+ const ageMs = now - lastEventMs;
1459
+ if (ageMs <= thresholdMs)
1460
+ continue;
1461
+ this.logger.warn(`[watchdog] killing stalled session ${name} (busy, no subprocess event for ${Math.round(ageMs / 1000)}s, threshold=${Math.round(thresholdMs / 1000)}s)`);
1462
+ try {
1463
+ trajectory.emit('session_stalled_killed', {
1464
+ ageMs,
1465
+ lastActivity: lastActivityIso,
1466
+ thresholdMs,
1467
+ model: managed.config.model,
1468
+ cwd: managed.cwd,
1469
+ isBusy: true,
1470
+ }, managed.claudeSessionId ?? name);
1471
+ }
1472
+ catch {
1473
+ // Trajectory is observability — must never block recovery
1474
+ }
1475
+ try {
1476
+ managed.session.stop();
1477
+ }
1478
+ catch {
1479
+ // Best-effort — subprocess may already be dead
1480
+ }
1481
+ this.sessions.delete(name);
1482
+ // Don't touch persistedSessions: the disk record may still be
1483
+ // resumable if the stall was transient (e.g. network blip during
1484
+ // model call). PERSIST_DISK_TTL_MS will GC it eventually.
1485
+ }
1486
+ }
1487
+ /**
1488
+ * Resolve the stalled-session kill threshold. Env var
1489
+ * `CC_OPENCLAW_STALLED_KILL_MS` overrides STALLED_SESSION_KILL_MS at
1490
+ * runtime so the value can be tuned without rebuild.
1491
+ */
1492
+ _stalledThresholdMs() {
1493
+ const raw = process.env.CC_OPENCLAW_STALLED_KILL_MS;
1494
+ if (raw) {
1495
+ const parsed = Number.parseInt(raw, 10);
1496
+ if (Number.isFinite(parsed) && parsed >= 10_000)
1497
+ return parsed;
1498
+ }
1499
+ return STALLED_SESSION_KILL_MS;
1500
+ }
1501
+ /**
1502
+ * v0.10.1 — record a fresh subprocess spawn for the runaway-loop
1503
+ * watchdog. Trims entries older than RUNAWAY_LOOP_WINDOW_MS so the
1504
+ * array length is bounded.
1505
+ */
1506
+ _recordSpawn() {
1507
+ const now = Date.now();
1508
+ this._recentSpawns.push(now);
1509
+ const cutoff = now - RUNAWAY_LOOP_WINDOW_MS;
1510
+ // Trim leading entries that have fallen out of the window. The array
1511
+ // stays sorted ascending since we only ever push Date.now().
1512
+ let drop = 0;
1513
+ while (drop < this._recentSpawns.length && this._recentSpawns[drop] < cutoff)
1514
+ drop++;
1515
+ if (drop > 0)
1516
+ this._recentSpawns.splice(0, drop);
1517
+ }
1518
+ /**
1519
+ * v0.10.1 — true when the spawn rate over RUNAWAY_LOOP_WINDOW_MS
1520
+ * exceeds the configured threshold (env-overridable). Called by
1521
+ * `_doStartSession` BEFORE recording the new spawn — i.e. the check
1522
+ * fires when the (N+1)-th spawn attempt would push the count over.
1523
+ */
1524
+ _isRunawayLoop() {
1525
+ // Trim stale entries before the rate decision so a long quiet period
1526
+ // followed by a burst doesn't carry stale counters.
1527
+ const now = Date.now();
1528
+ const cutoff = now - RUNAWAY_LOOP_WINDOW_MS;
1529
+ let drop = 0;
1530
+ while (drop < this._recentSpawns.length && this._recentSpawns[drop] < cutoff)
1531
+ drop++;
1532
+ if (drop > 0)
1533
+ this._recentSpawns.splice(0, drop);
1534
+ return this._recentSpawns.length >= this._runawayThreshold();
1535
+ }
1536
+ /**
1537
+ * v0.10.1 — resolve the runaway-loop spawn threshold. Env var
1538
+ * `CC_OPENCLAW_LOOP_MAX_SUBPROCS` overrides RUNAWAY_LOOP_MAX_SUBPROCS
1539
+ * for runtime tuning without rebuild. Clamped to [2, 20] to prevent
1540
+ * accidental disable (0/1) or unbounded raise.
1541
+ */
1542
+ _runawayThreshold() {
1543
+ const raw = process.env.CC_OPENCLAW_LOOP_MAX_SUBPROCS;
1544
+ if (raw) {
1545
+ const parsed = Number.parseInt(raw, 10);
1546
+ if (Number.isFinite(parsed) && parsed >= 2 && parsed <= 20)
1547
+ return parsed;
1548
+ }
1549
+ return RUNAWAY_LOOP_MAX_SUBPROCS;
1550
+ }
1387
1551
  }
1388
1552
  //# sourceMappingURL=session-manager.js.map