@openclaw/lobster 2026.9.3 → 2026.9.5

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 (322) hide show
  1. package/assets/activity.svg +4 -0
  2. package/assets/icon.png +0 -0
  3. package/dist/index.js +39 -10
  4. package/node_modules/@clawdbot/lobster/README.md +112 -1
  5. package/node_modules/@clawdbot/lobster/bin/clawd.invoke.js +13 -7
  6. package/node_modules/@clawdbot/lobster/bin/lobster.js +7 -7
  7. package/node_modules/@clawdbot/lobster/bin/openclaw.invoke.js +16 -10
  8. package/node_modules/@clawdbot/lobster/dist/src/abortable_process.js +297 -0
  9. package/node_modules/@clawdbot/lobster/dist/src/abortable_process.js.map +1 -0
  10. package/node_modules/@clawdbot/lobster/dist/src/cli.js +96 -248
  11. package/node_modules/@clawdbot/lobster/dist/src/cli.js.map +1 -1
  12. package/node_modules/@clawdbot/lobster/dist/src/commands/commands_list.js.map +1 -1
  13. package/node_modules/@clawdbot/lobster/dist/src/commands/registry.js +2 -0
  14. package/node_modules/@clawdbot/lobster/dist/src/commands/registry.js.map +1 -1
  15. package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/approve.js +2 -0
  16. package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/approve.js.map +1 -1
  17. package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/ask.js +3 -1
  18. package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/ask.js.map +1 -1
  19. package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/dedupe.js.map +1 -1
  20. package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/diff_last.js +6 -1
  21. package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/diff_last.js.map +1 -1
  22. package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/email_triage.js.map +1 -1
  23. package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/exec.js +30 -34
  24. package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/exec.js.map +1 -1
  25. package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/gog_gmail_search.js +12 -25
  26. package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/gog_gmail_search.js.map +1 -1
  27. package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/gog_gmail_send.js +19 -25
  28. package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/gog_gmail_send.js.map +1 -1
  29. package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/group_by.js.map +1 -1
  30. package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/head.js.map +1 -1
  31. package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/json.js.map +1 -1
  32. package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/llm_invoke.js +596 -42
  33. package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/llm_invoke.js.map +1 -1
  34. package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/map.js.map +1 -1
  35. package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/openclaw_agent.js +139 -0
  36. package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/openclaw_agent.js.map +1 -0
  37. package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/openclaw_invoke.js +20 -2
  38. package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/openclaw_invoke.js.map +1 -1
  39. package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/pick.js.map +1 -1
  40. package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/sort.js.map +1 -1
  41. package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/state.js +48 -20
  42. package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/state.js.map +1 -1
  43. package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/table.js.map +1 -1
  44. package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/template.js.map +1 -1
  45. package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/where.js.map +1 -1
  46. package/node_modules/@clawdbot/lobster/dist/src/commands/workflows/workflows_list.js.map +1 -1
  47. package/node_modules/@clawdbot/lobster/dist/src/commands/workflows/workflows_run.js.map +1 -1
  48. package/node_modules/@clawdbot/lobster/dist/src/core/cost_tracker.js +45 -3
  49. package/node_modules/@clawdbot/lobster/dist/src/core/cost_tracker.js.map +1 -1
  50. package/node_modules/@clawdbot/lobster/dist/src/core/filters.js.map +1 -1
  51. package/node_modules/@clawdbot/lobster/dist/src/core/retry.js.map +1 -1
  52. package/node_modules/@clawdbot/lobster/dist/src/core/tool_runtime.js +147 -21
  53. package/node_modules/@clawdbot/lobster/dist/src/core/tool_runtime.js.map +1 -1
  54. package/node_modules/@clawdbot/lobster/dist/src/input_request.js +4 -2
  55. package/node_modules/@clawdbot/lobster/dist/src/input_request.js.map +1 -1
  56. package/node_modules/@clawdbot/lobster/dist/src/parser.js +4 -3
  57. package/node_modules/@clawdbot/lobster/dist/src/parser.js.map +1 -1
  58. package/node_modules/@clawdbot/lobster/dist/src/pipeline_resume_state.js +241 -74
  59. package/node_modules/@clawdbot/lobster/dist/src/pipeline_resume_state.js.map +1 -1
  60. package/node_modules/@clawdbot/lobster/dist/src/read_line.js +52 -7
  61. package/node_modules/@clawdbot/lobster/dist/src/read_line.js.map +1 -1
  62. package/node_modules/@clawdbot/lobster/dist/src/read_response_text.js +86 -0
  63. package/node_modules/@clawdbot/lobster/dist/src/read_response_text.js.map +1 -0
  64. package/node_modules/@clawdbot/lobster/dist/src/recipes/github/pr-monitor.js +4 -2
  65. package/node_modules/@clawdbot/lobster/dist/src/recipes/github/pr-monitor.js.map +1 -1
  66. package/node_modules/@clawdbot/lobster/dist/src/recipes/github/stages/pr-view.js +20 -34
  67. package/node_modules/@clawdbot/lobster/dist/src/recipes/github/stages/pr-view.js.map +1 -1
  68. package/node_modules/@clawdbot/lobster/dist/src/recipes/registry.js.map +1 -1
  69. package/node_modules/@clawdbot/lobster/dist/src/renderers/json.js.map +1 -1
  70. package/node_modules/@clawdbot/lobster/dist/src/resume.js.map +1 -1
  71. package/node_modules/@clawdbot/lobster/dist/src/runtime.js +187 -17
  72. package/node_modules/@clawdbot/lobster/dist/src/runtime.js.map +1 -1
  73. package/node_modules/@clawdbot/lobster/dist/src/sdk/Lobster.js +10 -2
  74. package/node_modules/@clawdbot/lobster/dist/src/sdk/Lobster.js.map +1 -1
  75. package/node_modules/@clawdbot/lobster/dist/src/sdk/primitives/approve.js.map +1 -1
  76. package/node_modules/@clawdbot/lobster/dist/src/sdk/primitives/diff.js +13 -82
  77. package/node_modules/@clawdbot/lobster/dist/src/sdk/primitives/diff.js.map +1 -1
  78. package/node_modules/@clawdbot/lobster/dist/src/sdk/primitives/exec.js +22 -32
  79. package/node_modules/@clawdbot/lobster/dist/src/sdk/primitives/exec.js.map +1 -1
  80. package/node_modules/@clawdbot/lobster/dist/src/sdk/primitives/state.js +9 -101
  81. package/node_modules/@clawdbot/lobster/dist/src/sdk/primitives/state.js.map +1 -1
  82. package/node_modules/@clawdbot/lobster/dist/src/sdk/runtime.js +2 -1
  83. package/node_modules/@clawdbot/lobster/dist/src/sdk/runtime.js.map +1 -1
  84. package/node_modules/@clawdbot/lobster/dist/src/shell.js.map +1 -1
  85. package/node_modules/@clawdbot/lobster/dist/src/state/store.js +619 -19
  86. package/node_modules/@clawdbot/lobster/dist/src/state/store.js.map +1 -1
  87. package/node_modules/@clawdbot/lobster/dist/src/token.js.map +1 -1
  88. package/node_modules/@clawdbot/lobster/dist/src/validation.js.map +1 -1
  89. package/node_modules/@clawdbot/lobster/dist/src/workflows/file.js +963 -283
  90. package/node_modules/@clawdbot/lobster/dist/src/workflows/file.js.map +1 -1
  91. package/node_modules/@clawdbot/lobster/dist/src/workflows/github_pr_monitor.js +27 -28
  92. package/node_modules/@clawdbot/lobster/dist/src/workflows/github_pr_monitor.js.map +1 -1
  93. package/node_modules/@clawdbot/lobster/dist/src/workflows/graph.js +9 -2
  94. package/node_modules/@clawdbot/lobster/dist/src/workflows/graph.js.map +1 -1
  95. package/node_modules/@clawdbot/lobster/dist/src/workflows/registry.js.map +1 -1
  96. package/node_modules/@clawdbot/lobster/dist/test/approval_id.test.js.map +1 -1
  97. package/node_modules/@clawdbot/lobster/dist/test/approve_preview.test.js.map +1 -1
  98. package/node_modules/@clawdbot/lobster/dist/test/bin_smoke.test.js +25 -0
  99. package/node_modules/@clawdbot/lobster/dist/test/bin_smoke.test.js.map +1 -0
  100. package/node_modules/@clawdbot/lobster/dist/test/cancellation.test.js +5170 -0
  101. package/node_modules/@clawdbot/lobster/dist/test/cancellation.test.js.map +1 -0
  102. package/node_modules/@clawdbot/lobster/dist/test/clawd_invoke.test.js +22 -0
  103. package/node_modules/@clawdbot/lobster/dist/test/clawd_invoke.test.js.map +1 -1
  104. package/node_modules/@clawdbot/lobster/dist/test/clawd_invoke_legacy.test.js.map +1 -1
  105. package/node_modules/@clawdbot/lobster/dist/test/cli_run_file_args_json.test.js.map +1 -1
  106. package/node_modules/@clawdbot/lobster/dist/test/commands_list.test.js.map +1 -1
  107. package/node_modules/@clawdbot/lobster/dist/test/condition_comparison.test.js.map +1 -1
  108. package/node_modules/@clawdbot/lobster/dist/test/core_tool_runtime.test.js.map +1 -1
  109. package/node_modules/@clawdbot/lobster/dist/test/cost_tracker.test.js +1733 -1
  110. package/node_modules/@clawdbot/lobster/dist/test/cost_tracker.test.js.map +1 -1
  111. package/node_modules/@clawdbot/lobster/dist/test/dedupe.test.js.map +1 -1
  112. package/node_modules/@clawdbot/lobster/dist/test/diff_last.test.js.map +1 -1
  113. package/node_modules/@clawdbot/lobster/dist/test/doctor.test.js.map +1 -1
  114. package/node_modules/@clawdbot/lobster/dist/test/dry_run.test.js.map +1 -1
  115. package/node_modules/@clawdbot/lobster/dist/test/email_triage.test.js.map +1 -1
  116. package/node_modules/@clawdbot/lobster/dist/test/exec_stdin.test.js.map +1 -1
  117. package/node_modules/@clawdbot/lobster/dist/test/for_each.test.js +254 -0
  118. package/node_modules/@clawdbot/lobster/dist/test/for_each.test.js.map +1 -1
  119. package/node_modules/@clawdbot/lobster/dist/test/github_pr_notify_format.test.js.map +1 -1
  120. package/node_modules/@clawdbot/lobster/dist/test/github_pr_summary.test.js.map +1 -1
  121. package/node_modules/@clawdbot/lobster/dist/test/github_pr_view.test.js +176 -0
  122. package/node_modules/@clawdbot/lobster/dist/test/github_pr_view.test.js.map +1 -0
  123. package/node_modules/@clawdbot/lobster/dist/test/gog_gmail_send.test.js +100 -0
  124. package/node_modules/@clawdbot/lobster/dist/test/gog_gmail_send.test.js.map +1 -0
  125. package/node_modules/@clawdbot/lobster/dist/test/group_by.test.js.map +1 -1
  126. package/node_modules/@clawdbot/lobster/dist/test/helpers/wait_for_pid.js +19 -0
  127. package/node_modules/@clawdbot/lobster/dist/test/helpers/wait_for_pid.js.map +1 -0
  128. package/node_modules/@clawdbot/lobster/dist/test/http_response_body_cap.test.js +318 -0
  129. package/node_modules/@clawdbot/lobster/dist/test/http_response_body_cap.test.js.map +1 -0
  130. package/node_modules/@clawdbot/lobster/dist/test/llm_invoke.test.js +946 -1
  131. package/node_modules/@clawdbot/lobster/dist/test/llm_invoke.test.js.map +1 -1
  132. package/node_modules/@clawdbot/lobster/dist/test/llm_task_invoke.test.js +103 -0
  133. package/node_modules/@clawdbot/lobster/dist/test/llm_task_invoke.test.js.map +1 -1
  134. package/node_modules/@clawdbot/lobster/dist/test/map.test.js.map +1 -1
  135. package/node_modules/@clawdbot/lobster/dist/test/multi_approval_resume.test.js.map +1 -1
  136. package/node_modules/@clawdbot/lobster/dist/test/on_error.test.js.map +1 -1
  137. package/node_modules/@clawdbot/lobster/dist/test/openclaw_agent.test.js +169 -0
  138. package/node_modules/@clawdbot/lobster/dist/test/openclaw_agent.test.js.map +1 -0
  139. package/node_modules/@clawdbot/lobster/dist/test/openclaw_dispatch_retry.test.js +96 -0
  140. package/node_modules/@clawdbot/lobster/dist/test/openclaw_dispatch_retry.test.js.map +1 -0
  141. package/node_modules/@clawdbot/lobster/dist/test/openclaw_invoke_alias.test.js.map +1 -1
  142. package/node_modules/@clawdbot/lobster/dist/test/parallel.test.js.map +1 -1
  143. package/node_modules/@clawdbot/lobster/dist/test/parser.test.js +57 -0
  144. package/node_modules/@clawdbot/lobster/dist/test/parser.test.js.map +1 -1
  145. package/node_modules/@clawdbot/lobster/dist/test/process_output_limit.test.js +173 -0
  146. package/node_modules/@clawdbot/lobster/dist/test/process_output_limit.test.js.map +1 -0
  147. package/node_modules/@clawdbot/lobster/dist/test/read_line.test.js +54 -0
  148. package/node_modules/@clawdbot/lobster/dist/test/read_line.test.js.map +1 -1
  149. package/node_modules/@clawdbot/lobster/dist/test/release_changelog.test.js +39 -0
  150. package/node_modules/@clawdbot/lobster/dist/test/release_changelog.test.js.map +1 -0
  151. package/node_modules/@clawdbot/lobster/dist/test/request_input.test.js +1 -1
  152. package/node_modules/@clawdbot/lobster/dist/test/request_input.test.js.map +1 -1
  153. package/node_modules/@clawdbot/lobster/dist/test/resume.test.js.map +1 -1
  154. package/node_modules/@clawdbot/lobster/dist/test/sdk_exec.test.js +201 -0
  155. package/node_modules/@clawdbot/lobster/dist/test/sdk_exec.test.js.map +1 -0
  156. package/node_modules/@clawdbot/lobster/dist/test/sdk_lobster.test.js.map +1 -1
  157. package/node_modules/@clawdbot/lobster/dist/test/shell.test.js.map +1 -1
  158. package/node_modules/@clawdbot/lobster/dist/test/sort.test.js.map +1 -1
  159. package/node_modules/@clawdbot/lobster/dist/test/state.test.js +584 -1
  160. package/node_modules/@clawdbot/lobster/dist/test/state.test.js.map +1 -1
  161. package/node_modules/@clawdbot/lobster/dist/test/step_retry.test.js.map +1 -1
  162. package/node_modules/@clawdbot/lobster/dist/test/step_stdin_epipe.test.js +44 -0
  163. package/node_modules/@clawdbot/lobster/dist/test/step_stdin_epipe.test.js.map +1 -0
  164. package/node_modules/@clawdbot/lobster/dist/test/step_timeout.test.js +67 -1
  165. package/node_modules/@clawdbot/lobster/dist/test/step_timeout.test.js.map +1 -1
  166. package/node_modules/@clawdbot/lobster/dist/test/template.test.js.map +1 -1
  167. package/node_modules/@clawdbot/lobster/dist/test/template_filters.test.js +37 -0
  168. package/node_modules/@clawdbot/lobster/dist/test/template_filters.test.js.map +1 -1
  169. package/node_modules/@clawdbot/lobster/dist/test/tool_envelope_version.test.js.map +1 -1
  170. package/node_modules/@clawdbot/lobster/dist/test/tool_mode.test.js.map +1 -1
  171. package/node_modules/@clawdbot/lobster/dist/test/validation.test.js.map +1 -1
  172. package/node_modules/@clawdbot/lobster/dist/test/where.test.js +94 -0
  173. package/node_modules/@clawdbot/lobster/dist/test/where.test.js.map +1 -0
  174. package/node_modules/@clawdbot/lobster/dist/test/workflow_args_env.test.js +5 -3
  175. package/node_modules/@clawdbot/lobster/dist/test/workflow_args_env.test.js.map +1 -1
  176. package/node_modules/@clawdbot/lobster/dist/test/workflow_composition.test.js.map +1 -1
  177. package/node_modules/@clawdbot/lobster/dist/test/workflow_file.test.js +647 -1
  178. package/node_modules/@clawdbot/lobster/dist/test/workflow_file.test.js.map +1 -1
  179. package/node_modules/@clawdbot/lobster/dist/test/workflow_graph.test.js +12 -2
  180. package/node_modules/@clawdbot/lobster/dist/test/workflow_graph.test.js.map +1 -1
  181. package/node_modules/@clawdbot/lobster/dist/test/workflows.test.js.map +1 -1
  182. package/node_modules/@clawdbot/lobster/package.json +74 -73
  183. package/node_modules/typebox/build/compile/validator.d.mts +1 -1
  184. package/node_modules/typebox/build/compile/validator.mjs +1 -1
  185. package/node_modules/typebox/build/format/idna/idn-hostname.mjs +7 -5
  186. package/node_modules/typebox/build/format/iri.mjs +5 -1
  187. package/node_modules/typebox/build/guard/emit.mjs +2 -4
  188. package/node_modules/typebox/build/guard/guard.mjs +6 -18
  189. package/node_modules/typebox/build/guard/string.mjs +16 -24
  190. package/node_modules/typebox/build/schema/build.mjs +2 -2
  191. package/node_modules/typebox/build/schema/check.mjs +1 -1
  192. package/node_modules/typebox/build/schema/compile.d.mts +3 -2
  193. package/node_modules/typebox/build/schema/compile.mjs +8 -8
  194. package/node_modules/typebox/build/schema/engine/_context.d.mts +6 -7
  195. package/node_modules/typebox/build/schema/engine/_context.mjs +23 -15
  196. package/node_modules/typebox/build/schema/engine/_functions.d.mts +2 -2
  197. package/node_modules/typebox/build/schema/engine/_functions.mjs +1 -1
  198. package/node_modules/typebox/build/schema/engine/_reducer.d.mts +3 -3
  199. package/node_modules/typebox/build/schema/engine/_refine.d.mts +5 -5
  200. package/node_modules/typebox/build/schema/engine/_refine.mjs +4 -7
  201. package/node_modules/typebox/build/schema/engine/_stack.d.mts +34 -16
  202. package/node_modules/typebox/build/schema/engine/_stack.mjs +148 -104
  203. package/node_modules/typebox/build/schema/engine/additionalItems.d.mts +4 -4
  204. package/node_modules/typebox/build/schema/engine/additionalProperties.d.mts +8 -8
  205. package/node_modules/typebox/build/schema/engine/additionalProperties.mjs +26 -18
  206. package/node_modules/typebox/build/schema/engine/allOf.d.mts +4 -4
  207. package/node_modules/typebox/build/schema/engine/allOf.mjs +3 -3
  208. package/node_modules/typebox/build/schema/engine/anyOf.d.mts +4 -4
  209. package/node_modules/typebox/build/schema/engine/anyOf.mjs +5 -10
  210. package/node_modules/typebox/build/schema/engine/boolean.d.mts +4 -4
  211. package/node_modules/typebox/build/schema/engine/boolean.mjs +2 -6
  212. package/node_modules/typebox/build/schema/engine/const.d.mts +4 -4
  213. package/node_modules/typebox/build/schema/engine/const.mjs +2 -6
  214. package/node_modules/typebox/build/schema/engine/contains.d.mts +4 -4
  215. package/node_modules/typebox/build/schema/engine/contains.mjs +2 -6
  216. package/node_modules/typebox/build/schema/engine/dependencies.d.mts +4 -4
  217. package/node_modules/typebox/build/schema/engine/dependencies.mjs +3 -6
  218. package/node_modules/typebox/build/schema/engine/dependentRequired.d.mts +4 -4
  219. package/node_modules/typebox/build/schema/engine/dependentRequired.mjs +2 -6
  220. package/node_modules/typebox/build/schema/engine/dependentSchemas.d.mts +4 -4
  221. package/node_modules/typebox/build/schema/engine/dynamicRef.d.mts +4 -4
  222. package/node_modules/typebox/build/schema/engine/dynamicRef.mjs +11 -7
  223. package/node_modules/typebox/build/schema/engine/enum.d.mts +4 -4
  224. package/node_modules/typebox/build/schema/engine/enum.mjs +2 -6
  225. package/node_modules/typebox/build/schema/engine/exclusiveMaximum.d.mts +4 -4
  226. package/node_modules/typebox/build/schema/engine/exclusiveMaximum.mjs +2 -6
  227. package/node_modules/typebox/build/schema/engine/exclusiveMinimum.d.mts +4 -4
  228. package/node_modules/typebox/build/schema/engine/exclusiveMinimum.mjs +2 -6
  229. package/node_modules/typebox/build/schema/engine/format.d.mts +4 -4
  230. package/node_modules/typebox/build/schema/engine/format.mjs +2 -6
  231. package/node_modules/typebox/build/schema/engine/if.d.mts +4 -4
  232. package/node_modules/typebox/build/schema/engine/if.mjs +7 -15
  233. package/node_modules/typebox/build/schema/engine/items.d.mts +4 -4
  234. package/node_modules/typebox/build/schema/engine/items.mjs +1 -1
  235. package/node_modules/typebox/build/schema/engine/maxContains.d.mts +4 -4
  236. package/node_modules/typebox/build/schema/engine/maxContains.mjs +2 -6
  237. package/node_modules/typebox/build/schema/engine/maxItems.d.mts +4 -4
  238. package/node_modules/typebox/build/schema/engine/maxItems.mjs +2 -6
  239. package/node_modules/typebox/build/schema/engine/maxLength.d.mts +4 -4
  240. package/node_modules/typebox/build/schema/engine/maxLength.mjs +2 -6
  241. package/node_modules/typebox/build/schema/engine/maxProperties.d.mts +4 -4
  242. package/node_modules/typebox/build/schema/engine/maxProperties.mjs +2 -6
  243. package/node_modules/typebox/build/schema/engine/maximum.d.mts +4 -4
  244. package/node_modules/typebox/build/schema/engine/maximum.mjs +2 -6
  245. package/node_modules/typebox/build/schema/engine/minContains.d.mts +4 -4
  246. package/node_modules/typebox/build/schema/engine/minContains.mjs +2 -6
  247. package/node_modules/typebox/build/schema/engine/minItems.d.mts +5 -5
  248. package/node_modules/typebox/build/schema/engine/minItems.mjs +2 -6
  249. package/node_modules/typebox/build/schema/engine/minLength.d.mts +4 -4
  250. package/node_modules/typebox/build/schema/engine/minLength.mjs +2 -6
  251. package/node_modules/typebox/build/schema/engine/minProperties.d.mts +4 -4
  252. package/node_modules/typebox/build/schema/engine/minProperties.mjs +2 -6
  253. package/node_modules/typebox/build/schema/engine/minimum.d.mts +4 -4
  254. package/node_modules/typebox/build/schema/engine/minimum.mjs +2 -6
  255. package/node_modules/typebox/build/schema/engine/multipleOf.d.mts +4 -4
  256. package/node_modules/typebox/build/schema/engine/multipleOf.mjs +2 -6
  257. package/node_modules/typebox/build/schema/engine/not.d.mts +4 -4
  258. package/node_modules/typebox/build/schema/engine/not.mjs +4 -8
  259. package/node_modules/typebox/build/schema/engine/oneOf.d.mts +4 -4
  260. package/node_modules/typebox/build/schema/engine/oneOf.mjs +6 -11
  261. package/node_modules/typebox/build/schema/engine/pattern.d.mts +4 -4
  262. package/node_modules/typebox/build/schema/engine/pattern.mjs +3 -7
  263. package/node_modules/typebox/build/schema/engine/patternProperties.d.mts +4 -4
  264. package/node_modules/typebox/build/schema/engine/patternProperties.mjs +1 -1
  265. package/node_modules/typebox/build/schema/engine/prefixItems.d.mts +4 -4
  266. package/node_modules/typebox/build/schema/engine/prefixItems.mjs +1 -1
  267. package/node_modules/typebox/build/schema/engine/properties.d.mts +4 -4
  268. package/node_modules/typebox/build/schema/engine/properties.mjs +1 -1
  269. package/node_modules/typebox/build/schema/engine/propertyNames.d.mts +4 -4
  270. package/node_modules/typebox/build/schema/engine/propertyNames.mjs +4 -10
  271. package/node_modules/typebox/build/schema/engine/recursiveRef.d.mts +4 -4
  272. package/node_modules/typebox/build/schema/engine/recursiveRef.mjs +11 -7
  273. package/node_modules/typebox/build/schema/engine/ref.d.mts +4 -4
  274. package/node_modules/typebox/build/schema/engine/ref.mjs +19 -15
  275. package/node_modules/typebox/build/schema/engine/required.d.mts +4 -4
  276. package/node_modules/typebox/build/schema/engine/required.mjs +2 -6
  277. package/node_modules/typebox/build/schema/engine/schema.d.mts +7 -7
  278. package/node_modules/typebox/build/schema/engine/schema.mjs +137 -133
  279. package/node_modules/typebox/build/schema/engine/type.d.mts +4 -4
  280. package/node_modules/typebox/build/schema/engine/type.mjs +2 -6
  281. package/node_modules/typebox/build/schema/engine/unevaluatedItems.d.mts +4 -4
  282. package/node_modules/typebox/build/schema/engine/unevaluatedItems.mjs +5 -9
  283. package/node_modules/typebox/build/schema/engine/unevaluatedProperties.d.mts +4 -4
  284. package/node_modules/typebox/build/schema/engine/unevaluatedProperties.mjs +5 -9
  285. package/node_modules/typebox/build/schema/engine/uniqueItems.d.mts +4 -4
  286. package/node_modules/typebox/build/schema/engine/uniqueItems.mjs +2 -6
  287. package/node_modules/typebox/build/schema/errors.d.mts +2 -2
  288. package/node_modules/typebox/build/schema/errors.mjs +8 -14
  289. package/node_modules/typebox/build/schema/intern/index.d.mts +1 -0
  290. package/node_modules/typebox/build/schema/intern/index.mjs +1 -0
  291. package/node_modules/typebox/build/schema/intern/intern.d.mts +11 -0
  292. package/node_modules/typebox/build/schema/intern/intern.mjs +293 -0
  293. package/node_modules/typebox/build/schema/meta/draft_2019_09.d.mts +2 -0
  294. package/node_modules/typebox/build/schema/meta/draft_2019_09.mjs +288 -0
  295. package/node_modules/typebox/build/schema/meta/draft_2020_12.d.mts +2 -0
  296. package/node_modules/typebox/build/schema/meta/draft_2020_12.mjs +300 -0
  297. package/node_modules/typebox/build/schema/meta/draft_3.d.mts +2 -0
  298. package/node_modules/typebox/build/schema/meta/draft_3.mjs +143 -0
  299. package/node_modules/typebox/build/schema/meta/draft_4.d.mts +2 -0
  300. package/node_modules/typebox/build/schema/meta/draft_4.mjs +149 -0
  301. package/node_modules/typebox/build/schema/meta/draft_6.d.mts +2 -0
  302. package/node_modules/typebox/build/schema/meta/draft_6.mjs +155 -0
  303. package/node_modules/typebox/build/schema/meta/draft_7.d.mts +2 -0
  304. package/node_modules/typebox/build/schema/meta/draft_7.mjs +172 -0
  305. package/node_modules/typebox/build/schema/meta/index.d.mts +2 -0
  306. package/node_modules/typebox/build/schema/meta/index.mjs +2 -0
  307. package/node_modules/typebox/build/schema/meta/meta.d.mts +9 -0
  308. package/node_modules/typebox/build/schema/meta/meta.mjs +15 -0
  309. package/node_modules/typebox/build/schema/meta/schema.d.mts +6 -0
  310. package/node_modules/typebox/build/schema/meta/schema.mjs +1 -0
  311. package/node_modules/typebox/build/schema/parse.d.mts +1 -0
  312. package/node_modules/typebox/build/schema/parse.mjs +10 -5
  313. package/node_modules/typebox/build/schema/resolve/resolve.d.mts +8 -3
  314. package/node_modules/typebox/build/schema/resolve/resolve.mjs +268 -139
  315. package/node_modules/typebox/build/schema/schema.d.mts +2 -0
  316. package/node_modules/typebox/build/schema/schema.mjs +2 -0
  317. package/node_modules/typebox/build/value/errors/errors.d.mts +2 -12
  318. package/node_modules/typebox/build/value/errors/errors.mjs +2 -7
  319. package/node_modules/typebox/package.json +1 -1
  320. package/node_modules/typebox/readme.md +59 -89
  321. package/openclaw.plugin.json +1 -0
  322. package/package.json +10 -7
@@ -2,8 +2,10 @@ import path from "node:path";
2
2
  import { promises as fsp } from "node:fs";
3
3
  import { createHash } from "node:crypto";
4
4
  import { Ajv } from "ajv";
5
- import { ensureDirectory, isJsonSyntaxError, readStateJson, stableStringify, writeFileAtomic, writeStateJson, } from "../../state/store.js";
5
+ import { billableTokens } from "../../core/cost_tracker.js";
6
+ import { diffAndStore, ensureDirectory, atomicWriteWasPublished, isJsonSyntaxError, readStateJsonWithLock, stableStringify, withFileLock, writeFileAtomic, } from "../../state/store.js";
6
7
  import { createCompileCached } from "../../validation.js";
8
+ import { httpResponseLimitFromEnv, readResponseTextCapped } from "../../read_response_text.js";
7
9
  const ajv = new Ajv({ allErrors: true, strict: false });
8
10
  const compileCachedLocal = createCompileCached(ajv);
9
11
  const artifactSchema = {
@@ -89,6 +91,369 @@ const validatePayload = ajv.compile(payloadSchema);
89
91
  const validateResponseEnvelope = ajv.compile(responseSchema);
90
92
  const DEFAULT_MAX_VALIDATION_RETRIES = 1;
91
93
  const STATE_VERSION = 1;
94
+ // Identity version of the response cache key. See computeCacheKey.
95
+ const CACHE_KEY_VERSION = 2;
96
+ // Provenance for an item this module emitted: which stored answer it belongs to, and whether
97
+ // it came from a provider call or from run state / the response cache.
98
+ // A symbol key cannot come out of `JSON.parse`, so the JSON a workflow step reads from a
99
+ // command's stdout can never carry it: only objects built here, in this process, take part in
100
+ // the replay exemption in workflow cost accounting.
101
+ const LLM_PROVENANCE = Symbol("lobster.llm.provenance");
102
+ /**
103
+ * Stamps provenance on a value this module is emitting. The property is enumerable so a
104
+ * downstream `{ ...value }` keeps it, and `JSON.stringify` ignores symbol keys, so serialized
105
+ * output — including what is written to the cache — is unchanged.
106
+ */
107
+ function markProvenance(value, provenance) {
108
+ return Object.defineProperty(value, LLM_PROVENANCE, {
109
+ value: provenance,
110
+ enumerable: true,
111
+ configurable: true,
112
+ });
113
+ }
114
+ /**
115
+ * Marks an item this module is emitting, and its usage record with it. A projection such as
116
+ * `pick model,usage` builds a new object out of named fields, so the item's own mark does not
117
+ * reach the consumer — but the usage record crosses by reference, and the usage record is what
118
+ * gets billed. Marking both means provenance survives an in-process projection without the
119
+ * consumer having to recognize every command that can build one.
120
+ */
121
+ function markLlmItem(item, provenance) {
122
+ if (item.usage && typeof item.usage === "object") {
123
+ markProvenance(item.usage, provenance);
124
+ }
125
+ return markProvenance(item, provenance);
126
+ }
127
+ /**
128
+ * The provenance of an item — or of a usage record — this module produced in the current
129
+ * process, and null for anything else. The public `replayed` field is deliberately not
130
+ * consulted: any command can print it beside a real `usage` object, and honoring that would
131
+ * drop real spend from `_meta.cost` and `cost_limit`.
132
+ */
133
+ export function llmProvenanceOf(value) {
134
+ if (!value || typeof value !== "object")
135
+ return null;
136
+ const provenance = value[LLM_PROVENANCE];
137
+ if (!provenance || typeof provenance !== "object")
138
+ return null;
139
+ const { cacheKey, replayed } = provenance;
140
+ if (typeof cacheKey !== "string" || typeof replayed !== "boolean")
141
+ return null;
142
+ return { cacheKey, replayed };
143
+ }
144
+ /**
145
+ * Marks `target` — a structurally identical value rebuilt from `source`'s own JSON — as standing
146
+ * in for whatever calls `source` was marked with.
147
+ *
148
+ * The marks are deliberately not in that JSON, and re-deriving them from it would mean trusting a
149
+ * payload, which the accounting must never do. Carrying them is a different act: the caller holds
150
+ * both values in this process and knows one was built from the other, which nothing on disk can
151
+ * claim for itself.
152
+ *
153
+ * What is carried is always a replay, whatever the source was. A value read back out of storage
154
+ * re-emits an answer that already exists; the call behind it was made once, and this is not that
155
+ * call happening again.
156
+ */
157
+ export function carryLlmProvenance(source, target) {
158
+ if (!source || typeof source !== "object" || !target || typeof target !== "object")
159
+ return;
160
+ const provenance = llmProvenanceOf(source);
161
+ if (provenance)
162
+ markProvenance(target, { cacheKey: provenance.cacheKey, replayed: true });
163
+ if (Array.isArray(source) || Array.isArray(target)) {
164
+ if (!Array.isArray(source) || !Array.isArray(target))
165
+ return;
166
+ for (let index = 0; index < Math.min(source.length, target.length); index++) {
167
+ carryLlmProvenance(source[index], target[index]);
168
+ }
169
+ return;
170
+ }
171
+ for (const key of Object.keys(source)) {
172
+ carryLlmProvenance(source[key], target[key]);
173
+ }
174
+ }
175
+ /**
176
+ * Restores private replay provenance on completed step results loaded from Lobster's own
177
+ * workflow-resume state. Resume state is the narrow trusted boundary: ordinary command JSON
178
+ * never reaches this helper, so copying a public cache key cannot suppress its usage record.
179
+ */
180
+ export function restoreLlmProvenance(target, charges) {
181
+ if (!target || typeof target !== "object" || !Array.isArray(charges))
182
+ return;
183
+ const settled = charges.filter((charge) => charge &&
184
+ typeof charge === "object" &&
185
+ typeof charge.cacheKey === "string" &&
186
+ charge.cacheKey &&
187
+ charge.usage &&
188
+ typeof charge.usage === "object");
189
+ if (!settled.length)
190
+ return;
191
+ const visit = (value) => {
192
+ if (!value || typeof value !== "object")
193
+ return;
194
+ if (Array.isArray(value)) {
195
+ for (const item of value)
196
+ visit(item);
197
+ return;
198
+ }
199
+ const record = value;
200
+ const cacheKey = typeof record.cacheKey === "string" ? record.cacheKey : null;
201
+ const model = typeof record.model === "string" ? record.model : null;
202
+ const usage = record.usage;
203
+ if (cacheKey && usage && typeof usage === "object") {
204
+ const matches = settled.some((charge) => charge.cacheKey === cacheKey &&
205
+ (charge.model ?? null) === model &&
206
+ sameBillableUsage(charge.usage, usage));
207
+ if (matches) {
208
+ const provenance = { cacheKey, replayed: true };
209
+ markProvenance(usage, provenance);
210
+ markProvenance(record, provenance);
211
+ }
212
+ }
213
+ for (const child of Object.values(record))
214
+ visit(child);
215
+ };
216
+ visit(target);
217
+ }
218
+ // A call nothing ever bills — a step that failed outright — leaves its key behind, so the
219
+ // oldest are dropped to keep a long run's ledger bounded.
220
+ const MAX_UNBILLED_LIVE_INVOCATIONS = 256;
221
+ /**
222
+ * Creates the ledger for a single run. The run that paid for a call is the only one that can
223
+ * recover its charge, because no other run holds this ledger: a workflow reusing a cache entry
224
+ * written by an earlier run, or by an SDK caller outside cost accounting, finds nothing to
225
+ * claim and is billed nothing. A live call made without a ledger in `ctx` opens no charge.
226
+ *
227
+ * A run composed by another one — a `workflow:` step — passes the composing run's ledger as
228
+ * `parent`. Both bill the same answer at their own boundary: the child bills its step, the
229
+ * parent bills the output handed back to it. So a charge is opened in both and each settles
230
+ * its own copy exactly once, while a replay neither of them paid for still claims nothing.
231
+ */
232
+ export function createLlmSpendLedger(parent) {
233
+ // Outstanding charges per cache key, held one entry per call rather than as a count: two
234
+ // identical calls that race on a cold cache are two provider charges under one key, the
235
+ // replays that later stand in for them have to be able to settle both, and each carries the
236
+ // cost of the call that opened it.
237
+ const unbilled = new Map();
238
+ // Charges this run has already accounted for. A copy of an item that lost its mark can turn
239
+ // up in any later step, and the only way to tell it from a call nobody has billed yet is to
240
+ // remember what has been billed. Bounded like the open charges, oldest key first.
241
+ const billed = new Map();
242
+ function settle(cacheKey, charge) {
243
+ const seen = billed.get(cacheKey) ?? [];
244
+ seen.push(charge ?? {});
245
+ billed.set(cacheKey, seen);
246
+ for (const oldest of billed.keys()) {
247
+ if (billed.size <= MAX_UNBILLED_LIVE_INVOCATIONS)
248
+ break;
249
+ billed.delete(oldest);
250
+ }
251
+ }
252
+ return {
253
+ record(cacheKey, charge) {
254
+ if (!cacheKey)
255
+ return;
256
+ parent?.record(cacheKey, charge);
257
+ const open = unbilled.get(cacheKey) ?? [];
258
+ open.push({ ...charge });
259
+ unbilled.set(cacheKey, open);
260
+ for (const oldest of unbilled.keys()) {
261
+ if (unbilled.size <= MAX_UNBILLED_LIVE_INVOCATIONS)
262
+ break;
263
+ unbilled.delete(oldest);
264
+ }
265
+ },
266
+ /**
267
+ * Settles one outstanding charge and hands back what that call cost, or null for the
268
+ * caller that must not bill anything. A live item and every replay of it draw on the same
269
+ * charges, so a provider call is billed exactly once however many steps re-emit its
270
+ * answer — and N calls under one key can be billed N times, never fewer.
271
+ *
272
+ * The cost comes back because a replay is not a reliable witness of it: identical calls
273
+ * that raced on a cold cache each paid their own way, and every replay of them carries
274
+ * whichever single answer was stored.
275
+ */
276
+ claim(cacheKey, cost) {
277
+ const open = unbilled.get(cacheKey);
278
+ if (!open?.length)
279
+ return null;
280
+ // A caller that knows what its own call cost settles that call's charge. A step
281
+ // retried after an attempt that failed *after* paying has more than one charge under
282
+ // the key, and they did not cost the same: settling the wrong one leaves the other to
283
+ // be billed at this call's price instead of its own.
284
+ const own = cost
285
+ ? open.findIndex((charge) => (charge.model ?? null) === (cost.model ?? null) &&
286
+ sameBillableUsage(charge.usage, cost.usage))
287
+ : -1;
288
+ // Otherwise: a charge that records no cost settles nothing anyone can bill, so it
289
+ // must not be the one handed to a caller with a real call to account for. Charges
290
+ // restored from resume state written before they carried a cost are the ones this
291
+ // can be.
292
+ const index = own >= 0
293
+ ? own
294
+ : Math.max(open.findIndex((charge) => charge.usage !== undefined), 0);
295
+ const [charge] = open.splice(index, 1);
296
+ if (!open.length)
297
+ unbilled.delete(cacheKey);
298
+ settle(cacheKey, charge);
299
+ return charge ?? {};
300
+ },
301
+ /**
302
+ * Whether an item carrying a call's numbers but not its in-process mark should be billed.
303
+ * A matching open charge is settled so the same provider call is not recorded again by
304
+ * end-of-step cleanup. Once no charge is open, however, public fields can never suppress
305
+ * usage: only the private provenance symbol can prove an item is a replay.
306
+ */
307
+ billCopy(cacheKey, model, usage) {
308
+ const isSameCall = (charge) => (charge.model ?? null) === model && sameBillableUsage(charge.usage, usage);
309
+ // A copy that still names a cache key is read against that key alone: the key is
310
+ // evidence of which call it came from, and honoring it keeps one call's copy from
311
+ // settling another call's charge. A transform can emit `{ model, usage }` and drop
312
+ // the key with the symbols, leaving the cost as the only evidence there is.
313
+ for (const key of cacheKey === null ? [...unbilled.keys()] : [cacheKey]) {
314
+ const open = unbilled.get(key);
315
+ const index = open?.findIndex(isSameCall) ?? -1;
316
+ if (!open || index < 0)
317
+ continue;
318
+ settle(key, open.splice(index, 1)[0]);
319
+ if (!open.length)
320
+ unbilled.delete(key);
321
+ return true;
322
+ }
323
+ return true;
324
+ },
325
+ outstanding() {
326
+ const charges = [];
327
+ for (const [cacheKey, open] of unbilled) {
328
+ for (const charge of open)
329
+ charges.push({ cacheKey, count: 1, ...charge });
330
+ }
331
+ return charges;
332
+ },
333
+ /**
334
+ * Reopens charges a paused run had not billed. A pipeline can suspend mid-step — at an
335
+ * `ask` gate — after its LLM call has been paid for but before the step succeeded, so
336
+ * without this the charge would exist in no run: the paused one never billed it, and the
337
+ * resumed one would exempt the replay that stands in for it.
338
+ */
339
+ restore(charges) {
340
+ if (!Array.isArray(charges))
341
+ return;
342
+ for (const charge of charges) {
343
+ if (!charge || typeof charge !== "object")
344
+ continue;
345
+ if (typeof charge.cacheKey !== "string" || !charge.cacheKey)
346
+ continue;
347
+ const count = Math.floor(Number(charge.count ?? 0));
348
+ if (!Number.isFinite(count) || count < 1)
349
+ continue;
350
+ // State written before this field existed carries no cost, and a hand-edited file
351
+ // should not be able to invent one: only a plain object of numbers is taken.
352
+ const { cacheKey, count: _count, ...cost } = charge;
353
+ const restored = sanitizeChargeCost(cost);
354
+ for (let i = 0; i < count; i++)
355
+ this.record(cacheKey, restored);
356
+ }
357
+ },
358
+ /**
359
+ * The calls this run has already billed. A run that pauses carries this the way it carries
360
+ * what it spent: the total says how much, and this says which calls it was for.
361
+ */
362
+ settled() {
363
+ const charges = [];
364
+ for (const [cacheKey, seen] of billed) {
365
+ for (const charge of seen)
366
+ charges.push({ cacheKey, count: 1, ...charge });
367
+ }
368
+ return charges;
369
+ },
370
+ /**
371
+ * Restores what a paused run had billed, so the two halves of its accounting agree after
372
+ * the pause. `cost` brings the money back; without this the run that resumes has no record
373
+ * of what the money was for, and a later step that re-emits a completed LLM output — `head`
374
+ * over `$live.json` — hands on a copy the fresh ledger has never heard of. It is billed on
375
+ * top of the restored total: one provider call, twice in `_meta.cost` and against
376
+ * `cost_limit`.
377
+ *
378
+ * A settled charge is only ever read against a cache key, so what this restores can excuse
379
+ * a copy of a named call and nothing else. It cannot touch a live call — those settle out
380
+ * of the open charges — and the spend it is reconstructing is already in the `cost` this
381
+ * same state carries.
382
+ */
383
+ restoreSettled(charges) {
384
+ if (!Array.isArray(charges))
385
+ return;
386
+ for (const charge of charges) {
387
+ if (!charge || typeof charge !== "object")
388
+ continue;
389
+ if (typeof charge.cacheKey !== "string" || !charge.cacheKey)
390
+ continue;
391
+ const count = Math.floor(Number(charge.count ?? 0));
392
+ if (!Number.isFinite(count) || count < 1)
393
+ continue;
394
+ const { cacheKey, count: _count, ...cost } = charge;
395
+ const restored = sanitizeChargeCost(cost);
396
+ for (let i = 0; i < count; i++)
397
+ settle(cacheKey, restored);
398
+ }
399
+ },
400
+ };
401
+ }
402
+ /**
403
+ * Whether two usage records bill the same, asked of the accounting that bills them. Comparing
404
+ * the records themselves would answer a different question twice over: a live item's usage
405
+ * carries this module's provenance symbol, which a copy that went through JSON never can, and a
406
+ * stage that rebuilds an item can drop or recompute a field nothing is charged for.
407
+ */
408
+ function sameBillableUsage(left, right) {
409
+ if (!left || typeof left !== "object" || !right || typeof right !== "object")
410
+ return false;
411
+ const billed = billableTokens(left);
412
+ const other = billableTokens(right);
413
+ return billed.inputTokens === other.inputTokens && billed.outputTokens === other.outputTokens;
414
+ }
415
+ /**
416
+ * Opens a charge for a live call, and only for one that costs something. An answer that reports
417
+ * no usage is billed nowhere however many times it is replayed, so a charge for it would sit in
418
+ * the ledger with nothing to settle it -- and would be handed to the next caller with a real
419
+ * call to account for, leaving that one's charge open to be billed a second time.
420
+ */
421
+ function recordLiveCharge(ctx, cacheKey, item) {
422
+ const cost = chargeCostOf(item);
423
+ if (!cost)
424
+ return;
425
+ ledgerFrom(ctx)?.record(cacheKey, cost);
426
+ }
427
+ /** The cost of a live call, read from the item this module just built for it. */
428
+ function chargeCostOf(item) {
429
+ const usage = item?.usage;
430
+ if (!usage || typeof usage !== "object")
431
+ return undefined;
432
+ return { model: typeof item?.model === "string" ? item.model : null, usage };
433
+ }
434
+ function sanitizeChargeCost(cost) {
435
+ const usage = cost.usage;
436
+ if (!usage || typeof usage !== "object" || Array.isArray(usage))
437
+ return {};
438
+ const numbers = {};
439
+ for (const [key, value] of Object.entries(usage)) {
440
+ if (typeof value === "number" && Number.isFinite(value))
441
+ numbers[key] = value;
442
+ }
443
+ if (!Object.keys(numbers).length)
444
+ return {};
445
+ return {
446
+ ...(typeof cost.stepId === "string" ? { stepId: cost.stepId } : null),
447
+ model: typeof cost.model === "string" ? cost.model : null,
448
+ usage: numbers,
449
+ };
450
+ }
451
+ function ledgerFrom(ctx) {
452
+ const ledger = ctx?.llmSpendLedger;
453
+ if (!ledger || typeof ledger !== "object")
454
+ return null;
455
+ return typeof ledger.record === "function" && typeof ledger.claim === "function" ? ledger : null;
456
+ }
92
457
  export const llmInvokeCommand = createLlmInvokeCommand({
93
458
  name: "llm.invoke",
94
459
  itemKind: "llm.invoke",
@@ -164,7 +529,7 @@ export function createLlmInvokeCommand(config) {
164
529
  "schema-version": { type: "string", description: "Logical schema version for caching" },
165
530
  "max-validation-retries": {
166
531
  type: "number",
167
- description: "Retries when schema validation fails",
532
+ description: "Extra model calls allowed after the first when schema validation fails (default 1)",
168
533
  },
169
534
  temperature: { type: "number", description: "Sampling temperature" },
170
535
  "max-output-tokens": { type: "number", description: "Max completion tokens" },
@@ -190,7 +555,8 @@ export function createLlmInvokeCommand(config) {
190
555
  "Features:",
191
556
  " - Typed payload validation before invoking the adapter.",
192
557
  " - Run-state + file cache so resumes do not re-call the LLM.",
193
- " - Optional JSON-schema enforcement with bounded retries.",
558
+ " - Optional JSON-schema enforcement, retried at most --max-validation-retries times",
559
+ " after the first call.",
194
560
  "",
195
561
  "Config:",
196
562
  ...config.helpConfig.map((line) => ` - ${line}`),
@@ -204,6 +570,10 @@ export function createLlmInvokeCommand(config) {
204
570
  }
205
571
  async function runLlmInvoke({ input, args, ctx, config, }) {
206
572
  const env = ctx.env ?? process.env;
573
+ const signal = ctx?.signal;
574
+ // Run-state and cache hits return before any adapter call, so a cancelled run
575
+ // would otherwise still finish as a success.
576
+ throwIfCancelled(signal);
207
577
  const provider = resolveProvider(args, env, config.defaultProvider, ctx);
208
578
  const adapter = resolveAdapter({ provider, env, args, config, ctx });
209
579
  const prompt = extractPrompt(args);
@@ -236,6 +606,9 @@ async function runLlmInvoke({ input, args, ctx, config, }) {
236
606
  const inputArtifacts = [];
237
607
  for await (const item of input)
238
608
  inputArtifacts.push(item);
609
+ // Draining pipeline input waits on the upstream step, so a timeout can fire
610
+ // here. Re-check before the reuse lookups below can answer with a success.
611
+ throwIfCancelled(signal);
239
612
  const normalizedArtifacts = [...inputArtifacts, ...providedArtifacts].map(normalizeArtifact);
240
613
  const artifactHashes = normalizedArtifacts.map(hashArtifact);
241
614
  const cacheKey = computeCacheKey({
@@ -245,21 +618,29 @@ async function runLlmInvoke({ input, args, ctx, config, }) {
245
618
  schemaVersion,
246
619
  artifactHashes,
247
620
  outputSchema: userOutputSchema,
621
+ temperature,
622
+ maxOutputTokens,
248
623
  });
249
624
  if (stateKey && !forceRefresh) {
250
- const stored = await readReusableLlmState(env, stateKey);
625
+ const stored = await readReusableLlmState(env, stateKey, ctx.signal);
626
+ // Reading run state is I/O of unbounded duration; a signal that aborted
627
+ // during it must not be overtaken by the replay below.
628
+ throwIfCancelled(signal);
251
629
  const reused = pickReusableState(stored, cacheKey, config.stateType);
252
630
  if (reused) {
631
+ const replay = { cacheKey, replayed: true };
253
632
  return {
254
- output: streamOf(reused.items.map((item) => ({ ...item, source: "run_state", cached: true }))),
633
+ output: streamOf(reused.items.map((item) => markLlmItem({ ...item, source: "run_state", cached: true, replayed: true }, replay))),
255
634
  };
256
635
  }
257
636
  }
258
637
  if (!disableCache && !forceRefresh) {
259
- const cache = await readCacheEntry(env, cacheKey, config.cacheNamespace);
638
+ const cache = await readCacheEntry(env, cacheKey, config.cacheNamespace, ctx.signal);
639
+ throwIfCancelled(signal);
260
640
  if (cache) {
641
+ const replay = { cacheKey, replayed: true };
261
642
  return {
262
- output: streamOf(cache.items.map((item) => ({ ...item, source: "cache", cached: true }))),
643
+ output: streamOf(cache.items.map((item) => markLlmItem({ ...item, source: "cache", cached: true, replayed: true }, replay))),
263
644
  };
264
645
  }
265
646
  }
@@ -286,6 +667,7 @@ async function runLlmInvoke({ input, args, ctx, config, }) {
286
667
  let attempt = 0;
287
668
  let lastValidationErrors = [];
288
669
  while (true) {
670
+ throwIfCancelled(signal);
289
671
  attempt += 1;
290
672
  if (attempt > 1) {
291
673
  payload.retryContext = {
@@ -298,9 +680,16 @@ async function runLlmInvoke({ input, args, ctx, config, }) {
298
680
  }
299
681
  let responseEnvelope;
300
682
  try {
301
- responseEnvelope = await adapter.invoke({ env, args, payload });
683
+ ctx.signal?.throwIfAborted();
684
+ responseEnvelope = await abortable(adapter.invoke({ env, args, payload, signal: ctx.signal }), signal);
685
+ ctx.signal?.throwIfAborted();
302
686
  }
303
687
  catch (err) {
688
+ // Cancellation is the caller's error, not an adapter failure: surface it
689
+ // as an abort so workflow timeout and abort handling still recognizes it,
690
+ // rather than wrapping it in a "request failed" adapter error.
691
+ if (signal?.aborted)
692
+ throw asCancellation(err, signal);
304
693
  throw new Error(`${config.name} request failed: ${err?.message ?? String(err)}`);
305
694
  }
306
695
  if (!validateResponseEnvelope(responseEnvelope)) {
@@ -319,37 +708,112 @@ async function runLlmInvoke({ input, args, ctx, config, }) {
319
708
  attempt,
320
709
  itemKind: config.itemKind,
321
710
  });
711
+ const live = { cacheKey, replayed: false };
712
+ for (const item of normalized)
713
+ markLlmItem(item, live);
714
+ // The provider has answered and been paid, so the charge is opened here rather than at
715
+ // either of the returns below. An attempt the local validator rejects never reaches one
716
+ // of them -- it goes round the loop and asks again -- and its call was as real as the one
717
+ // that eventually satisfies the schema. Opening it here also puts it before the writes
718
+ // that store the answer: either can fail once run state already holds a replayable copy,
719
+ // and the retry that replays it must still find a charge to settle.
720
+ recordLiveCharge(ctx, cacheKey, normalized[0]);
322
721
  if (!validator) {
722
+ ctx.signal?.throwIfAborted();
323
723
  await persistOutputs({
324
724
  env,
325
725
  stateKey,
326
726
  cacheKey,
327
727
  items: normalized,
328
728
  stateType: config.stateType,
729
+ signal: ctx.signal,
730
+ afterStore: disableCache
731
+ ? undefined
732
+ : () => writeCacheEntry(env, cacheKey, normalized, config.cacheNamespace, ctx.signal),
329
733
  });
330
- if (!disableCache)
331
- await writeCacheEntry(env, cacheKey, normalized, config.cacheNamespace);
332
734
  return { output: streamOf(normalized) };
333
735
  }
334
736
  const structured = normalized[0]?.output?.data ?? null;
335
737
  if (validator(structured)) {
738
+ ctx.signal?.throwIfAborted();
336
739
  await persistOutputs({
337
740
  env,
338
741
  stateKey,
339
742
  cacheKey,
340
743
  items: normalized,
341
744
  stateType: config.stateType,
745
+ signal: ctx.signal,
746
+ afterStore: disableCache
747
+ ? undefined
748
+ : () => writeCacheEntry(env, cacheKey, normalized, config.cacheNamespace, ctx.signal),
342
749
  });
343
- if (!disableCache)
344
- await writeCacheEntry(env, cacheKey, normalized, config.cacheNamespace);
345
750
  return { output: streamOf(normalized) };
346
751
  }
347
752
  lastValidationErrors = collectAjvErrors(validator.errors);
348
- if (attempt > maxValidationRetries + 1) {
753
+ if (attempt > maxValidationRetries) {
349
754
  throw new Error(`${config.name} output failed schema validation: ${lastValidationErrors.join("; ")}`);
350
755
  }
351
756
  }
352
757
  }
758
+ /**
759
+ * Build the error a cancelled run rejects with.
760
+ *
761
+ * The workflow runner only treats an error named `AbortError` or coded
762
+ * `ABORT_ERR` as cancellation; everything else follows the step's retry and
763
+ * `on_error` policy. A host may abort with any reason it likes, so passing
764
+ * `signal.reason` straight through means `controller.abort(new Error("stop"))`
765
+ * leaves a cancelled step looking like an ordinary failure -- retried, or
766
+ * swallowed by `on_error: continue` and reported as a successful run. Keep the
767
+ * host's message and hang its reason off `cause`, but mark the rejection so the
768
+ * runner recognizes it.
769
+ */
770
+ function cancellationError(signal) {
771
+ const reason = signal.reason;
772
+ if (reason === undefined || reason === null) {
773
+ return new DOMException("The operation was aborted.", "AbortError");
774
+ }
775
+ if (reason?.name === "AbortError" || reason?.code === "ABORT_ERR")
776
+ return reason;
777
+ const message = typeof reason?.message === "string" && reason.message ? reason.message : String(reason);
778
+ const error = new Error(message, { cause: reason });
779
+ error.name = "AbortError";
780
+ error.code = "ABORT_ERR";
781
+ return error;
782
+ }
783
+ /** Rethrow `err` when it already reads as cancellation, else the run's reason. */
784
+ function asCancellation(err, signal) {
785
+ if (err?.name === "AbortError" || err?.code === "ABORT_ERR")
786
+ return err;
787
+ return cancellationError(signal);
788
+ }
789
+ function throwIfCancelled(signal) {
790
+ if (signal?.aborted)
791
+ throw cancellationError(signal);
792
+ }
793
+ /**
794
+ * Reject as soon as the run is cancelled instead of waiting for `promise`.
795
+ * HTTP adapters are cancelled at the socket, but an injected `ctx.llmAdapters`
796
+ * adapter may ignore `ctx.signal` entirely; without this, one of those keeps a
797
+ * timed-out step waiting for as long as it likes.
798
+ */
799
+ function abortable(promise, signal) {
800
+ if (!signal)
801
+ return promise;
802
+ return new Promise((resolve, reject) => {
803
+ const onAbort = () => reject(cancellationError(signal));
804
+ // Observe `promise` before anything can settle the wrapper, including the
805
+ // already-aborted case below. An adapter can cancel the run from inside its own
806
+ // `invoke` and reject afterwards; rejecting here without watching that promise
807
+ // leaves the rejection unhandled, which ends the process under Node's default
808
+ // handling -- long after this step was cancelled cleanly.
809
+ promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
810
+ if (signal.aborted) {
811
+ onAbort();
812
+ return;
813
+ }
814
+ signal.addEventListener("abort", onAbort, { once: true });
815
+ });
816
+ }
353
817
  function resolveProvider(args, env, defaultProvider, ctx) {
354
818
  const explicit = String(args.provider ?? env.LOBSTER_LLM_PROVIDER ?? "")
355
819
  .trim()
@@ -385,11 +849,12 @@ function resolveAdapter({ provider, env, args, config, ctx, }) {
385
849
  return {
386
850
  provider,
387
851
  source: typeof direct === "function" ? provider : (direct.source ?? provider),
388
- async invoke({ payload }) {
389
- return invoke({ env, args, payload, ctx });
852
+ async invoke({ payload, signal }) {
853
+ return invoke({ env, args, payload, ctx, signal });
390
854
  },
391
855
  };
392
856
  }
857
+ const maxResponseBytes = httpResponseLimitFromEnv(env);
393
858
  if (provider === "openclaw") {
394
859
  const openclawUrl = String(env.OPENCLAW_URL ?? env.CLAWD_URL ?? "").trim();
395
860
  if (!openclawUrl) {
@@ -400,8 +865,8 @@ function resolveAdapter({ provider, env, args, config, ctx, }) {
400
865
  return {
401
866
  provider,
402
867
  source: config.sourceForProvider?.(provider) ?? "openclaw",
403
- async invoke({ payload }) {
404
- return invokeOpenClawAdapter({ endpoint, token, payload });
868
+ async invoke({ payload, signal }) {
869
+ return invokeOpenClawAdapter({ endpoint, token, payload, signal, maxResponseBytes });
405
870
  },
406
871
  };
407
872
  }
@@ -414,8 +879,14 @@ function resolveAdapter({ provider, env, args, config, ctx, }) {
414
879
  return {
415
880
  provider,
416
881
  source: config.sourceForProvider?.(provider) ?? "pi",
417
- async invoke({ payload }) {
418
- return invokeHttpAdapter({ endpoint: buildAdapterEndpoint(adapterUrl), token, payload });
882
+ async invoke({ payload, signal }) {
883
+ return invokeHttpAdapter({
884
+ endpoint: buildAdapterEndpoint(adapterUrl),
885
+ maxResponseBytes,
886
+ token,
887
+ payload,
888
+ signal,
889
+ });
419
890
  },
420
891
  };
421
892
  }
@@ -427,8 +898,14 @@ function resolveAdapter({ provider, env, args, config, ctx, }) {
427
898
  return {
428
899
  provider,
429
900
  source: config.sourceForProvider?.(provider) ?? "http",
430
- async invoke({ payload }) {
431
- return invokeHttpAdapter({ endpoint: buildAdapterEndpoint(adapterUrl), token, payload });
901
+ async invoke({ payload, signal }) {
902
+ return invokeHttpAdapter({
903
+ endpoint: buildAdapterEndpoint(adapterUrl),
904
+ maxResponseBytes,
905
+ token,
906
+ payload,
907
+ signal,
908
+ });
432
909
  },
433
910
  };
434
911
  }
@@ -451,9 +928,10 @@ function buildAdapterEndpoint(rawUrl) {
451
928
  }
452
929
  return endpoint;
453
930
  }
454
- async function invokeOpenClawAdapter({ endpoint, token, payload, }) {
931
+ async function invokeOpenClawAdapter({ endpoint, token, payload, signal, maxResponseBytes, }) {
455
932
  const res = await fetch(endpoint, {
456
933
  method: "POST",
934
+ signal,
457
935
  headers: {
458
936
  "content-type": "application/json",
459
937
  ...(token ? { authorization: `Bearer ${token}` } : null),
@@ -464,7 +942,7 @@ async function invokeOpenClawAdapter({ endpoint, token, payload, }) {
464
942
  args: payload,
465
943
  }),
466
944
  });
467
- const text = await res.text();
945
+ const text = await readResponseTextCapped(res, maxResponseBytes);
468
946
  if (!res.ok) {
469
947
  throw new Error(`${res.status} ${res.statusText}: ${text.slice(0, 400)}`);
470
948
  }
@@ -488,16 +966,17 @@ async function invokeOpenClawAdapter({ endpoint, token, payload, }) {
488
966
  }
489
967
  return { ok: true, result: parsed };
490
968
  }
491
- async function invokeHttpAdapter({ endpoint, token, payload, }) {
969
+ async function invokeHttpAdapter({ endpoint, token, payload, signal, maxResponseBytes, }) {
492
970
  const res = await fetch(endpoint, {
493
971
  method: "POST",
972
+ signal,
494
973
  headers: {
495
974
  "content-type": "application/json",
496
975
  ...(token ? { authorization: `Bearer ${token}` } : null),
497
976
  },
498
977
  body: JSON.stringify(payload),
499
978
  });
500
- const text = await res.text();
979
+ const text = await readResponseTextCapped(res, maxResponseBytes);
501
980
  if (!res.ok) {
502
981
  throw new Error(`${res.status} ${res.statusText}: ${text.slice(0, 400)}`);
503
982
  }
@@ -603,14 +1082,24 @@ function hashArtifact(artifact) {
603
1082
  const stable = stableStringify(artifact);
604
1083
  return createHash("sha256").update(stable).digest("hex");
605
1084
  }
606
- function computeCacheKey({ provider, prompt, model, schemaVersion, artifactHashes, outputSchema, }) {
1085
+ function computeCacheKey({ provider, prompt, model, schemaVersion, artifactHashes, outputSchema, temperature, maxOutputTokens, }) {
1086
+ // `null` is the identity of an omitted parameter, distinct from any value a caller can
1087
+ // pass, so an omitted request can never resolve to an entry written with an explicit one.
1088
+ // The version separates this identity from the keys earlier releases wrote, where the two
1089
+ // parameters were absent from the payload and a sampled answer therefore shared a key with
1090
+ // an unsampled request. Bump it whenever the fields below change: entries under older
1091
+ // versions become unreachable, which costs one re-invocation and never a wrong replay.
607
1092
  const payload = {
1093
+ cacheKeyVersion: CACHE_KEY_VERSION,
608
1094
  provider,
609
1095
  prompt,
610
1096
  model: model || `${provider}-default`,
611
1097
  schemaVersion,
612
1098
  artifactHashes,
613
1099
  outputSchema: outputSchema ?? null,
1100
+ // The same predicate that decides whether each parameter is sent to the adapter.
1101
+ temperature: Number.isFinite(temperature ?? NaN) ? Number(temperature) : null,
1102
+ maxOutputTokens: Number.isFinite(maxOutputTokens ?? NaN) ? Number(maxOutputTokens) : null,
614
1103
  };
615
1104
  return createHash("sha256").update(stableStringify(payload)).digest("hex");
616
1105
  }
@@ -646,9 +1135,11 @@ function normalizeResult({ envelope, cacheKey, schemaVersion, artifactHashes, so
646
1135
  };
647
1136
  return [item];
648
1137
  }
649
- async function persistOutputs({ env, stateKey, cacheKey, items, stateType, }) {
650
- if (!stateKey)
1138
+ async function persistOutputs({ env, stateKey, cacheKey, items, stateType, signal, afterStore, }) {
1139
+ if (!stateKey) {
1140
+ await afterStore?.();
651
1141
  return;
1142
+ }
652
1143
  const record = {
653
1144
  type: stateType,
654
1145
  version: STATE_VERSION,
@@ -656,11 +1147,17 @@ async function persistOutputs({ env, stateKey, cacheKey, items, stateType, }) {
656
1147
  items,
657
1148
  storedAt: new Date().toISOString(),
658
1149
  };
659
- await writeStateJson({ env, key: stateKey, value: record });
1150
+ await diffAndStore({
1151
+ env,
1152
+ key: stateKey,
1153
+ value: record,
1154
+ signal,
1155
+ afterStore: afterStore ? () => afterStore() : undefined,
1156
+ });
660
1157
  }
661
- async function readReusableLlmState(env, stateKey) {
1158
+ async function readReusableLlmState(env, stateKey, signal) {
662
1159
  try {
663
- return await readStateJson({ env, key: stateKey });
1160
+ return await readStateJsonWithLock({ env, key: stateKey, signal });
664
1161
  }
665
1162
  catch (err) {
666
1163
  if (isJsonSyntaxError(err))
@@ -684,28 +1181,85 @@ function collectAjvErrors(errors) {
684
1181
  return [];
685
1182
  return errors.map((err) => `${err.instancePath || "/"} ${err.message ?? ""}`.trim());
686
1183
  }
687
- async function readCacheEntry(env, key, cacheNamespace) {
1184
+ async function readCacheEntry(env, key, cacheNamespace, signal) {
688
1185
  const filePath = path.join(getCacheDir(env), cacheNamespace, `${key}.json`);
1186
+ const read = async () => {
1187
+ try {
1188
+ const text = await fsp.readFile(filePath, "utf8");
1189
+ const parsed = JSON.parse(text);
1190
+ if (parsed?.cacheKey !== key || !Array.isArray(parsed.items))
1191
+ return null;
1192
+ return parsed;
1193
+ }
1194
+ catch (err) {
1195
+ if (err?.code === "ENOENT")
1196
+ return null;
1197
+ if (isJsonSyntaxError(err))
1198
+ return null;
1199
+ throw err;
1200
+ }
1201
+ };
689
1202
  try {
690
- const text = await fsp.readFile(filePath, "utf8");
691
- const parsed = JSON.parse(text);
692
- if (parsed?.cacheKey !== key || !Array.isArray(parsed.items))
693
- return null;
694
- return parsed;
1203
+ return await withFileLock({ filePath, signal, task: read });
695
1204
  }
696
1205
  catch (err) {
697
- if (err?.code === "ENOENT")
698
- return null;
699
- if (isJsonSyntaxError(err))
1206
+ if (err?.code === "ENOENT" || err?.code === "ENOTDIR")
700
1207
  return null;
1208
+ // A cache mounted read-only cannot have an active local writer because a
1209
+ // writer first creates the same coordination lock. Preserve its reusable
1210
+ // entries instead of requiring a lock-directory write for a read.
1211
+ if (["EACCES", "EPERM", "EROFS"].includes(err?.code))
1212
+ return read();
701
1213
  throw err;
702
1214
  }
703
1215
  }
704
- async function writeCacheEntry(env, key, items, cacheNamespace) {
1216
+ async function writeCacheEntry(env, key, items, cacheNamespace, signal) {
705
1217
  const dir = path.join(getCacheDir(env), cacheNamespace);
1218
+ signal?.throwIfAborted();
706
1219
  await ensureDirectory(dir);
707
1220
  const filePath = path.join(dir, `${key}.json`);
708
- await writeFileAtomic(filePath, JSON.stringify({ items, cacheKey: key, storedAt: new Date().toISOString() }, null, 2) + "\n");
1221
+ const content = JSON.stringify({ items, cacheKey: key, storedAt: new Date().toISOString() }, null, 2) + "\n";
1222
+ await withFileLock({
1223
+ filePath,
1224
+ signal,
1225
+ task: async () => {
1226
+ let previousContent = null;
1227
+ try {
1228
+ previousContent = await fsp.readFile(filePath);
1229
+ }
1230
+ catch (err) {
1231
+ if (err?.code !== "ENOENT")
1232
+ throw err;
1233
+ }
1234
+ const restorePreviousContent = async () => {
1235
+ // No cache reader or competing cache writer can observe this entry
1236
+ // until this lock is released. Restore an entry replaced by a refresh;
1237
+ // only remove the just-published file when none existed beforehand.
1238
+ if (previousContent === null)
1239
+ await fsp.rm(filePath, { force: true });
1240
+ else
1241
+ await writeFileAtomic(filePath, previousContent);
1242
+ };
1243
+ let cacheWasPublished = false;
1244
+ try {
1245
+ signal?.throwIfAborted();
1246
+ const result = await writeFileAtomic(filePath, content, { signal });
1247
+ cacheWasPublished = true;
1248
+ if (result?.signalAbortedAfterCommit || signal?.aborted) {
1249
+ await restorePreviousContent();
1250
+ cacheWasPublished = false;
1251
+ signal?.throwIfAborted();
1252
+ throw new Error("LLM cache publication cancelled");
1253
+ }
1254
+ }
1255
+ catch (err) {
1256
+ if (cacheWasPublished || atomicWriteWasPublished(err)) {
1257
+ await restorePreviousContent();
1258
+ }
1259
+ throw err;
1260
+ }
1261
+ },
1262
+ });
709
1263
  }
710
1264
  function getCacheDir(env) {
711
1265
  if (env?.LOBSTER_CACHE_DIR)