@openclaw/lobster 2026.9.4 → 2026.9.6
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.
- package/assets/activity.svg +4 -0
- package/assets/icon.png +0 -0
- package/dist/index.js +39 -10
- package/node_modules/@clawdbot/lobster/README.md +129 -131
- package/node_modules/@clawdbot/lobster/VISION.md +15 -238
- package/node_modules/@clawdbot/lobster/bin/clawd.invoke.js +2 -15
- package/node_modules/@clawdbot/lobster/bin/invoke.js +19 -0
- package/node_modules/@clawdbot/lobster/bin/lobster.js +2 -21
- package/node_modules/@clawdbot/lobster/bin/openclaw.invoke.js +2 -18
- package/node_modules/@clawdbot/lobster/dist/src/abortable_process.js +297 -0
- package/node_modules/@clawdbot/lobster/dist/src/abortable_process.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/cli.js +128 -296
- package/node_modules/@clawdbot/lobster/dist/src/cli.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/command_tokens.js +81 -0
- package/node_modules/@clawdbot/lobster/dist/src/command_tokens.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/commands/commands_list.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/registry.js +2 -0
- package/node_modules/@clawdbot/lobster/dist/src/commands/registry.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/approve.js +2 -0
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/approve.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/ask.js +3 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/ask.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/dedupe.js +1 -12
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/dedupe.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/diff_last.js +6 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/diff_last.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/email_triage.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/exec.js +30 -34
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/exec.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/gog_gmail_search.js +12 -25
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/gog_gmail_search.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/gog_gmail_send.js +19 -25
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/gog_gmail_send.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/group_by.js +2 -14
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/group_by.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/head.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/json.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/llm_invoke.js +260 -42
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/llm_invoke.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/map.js +2 -13
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/map.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/openclaw_agent.js +139 -0
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/openclaw_agent.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/openclaw_invoke.js +20 -2
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/openclaw_invoke.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/pick.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/sort.js +4 -21
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/sort.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/state.js +48 -20
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/state.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/table.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/template.js +2 -13
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/template.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/where.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/workflows/workflows_list.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/workflows/workflows_run.js +0 -18
- package/node_modules/@clawdbot/lobster/dist/src/commands/workflows/workflows_run.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/core/cost_tracker.js +45 -3
- package/node_modules/@clawdbot/lobster/dist/src/core/cost_tracker.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/core/filters.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/core/llm_accounting.js +304 -0
- package/node_modules/@clawdbot/lobster/dist/src/core/llm_accounting.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/core/retry.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/core/tool_runtime.js +147 -21
- package/node_modules/@clawdbot/lobster/dist/src/core/tool_runtime.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/core/value_path.js +11 -0
- package/node_modules/@clawdbot/lobster/dist/src/core/value_path.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/input_request.js +4 -2
- package/node_modules/@clawdbot/lobster/dist/src/input_request.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/parser.js +5 -69
- package/node_modules/@clawdbot/lobster/dist/src/parser.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/pipeline_resume_state.js +241 -74
- package/node_modules/@clawdbot/lobster/dist/src/pipeline_resume_state.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/read_line.js +52 -7
- package/node_modules/@clawdbot/lobster/dist/src/read_line.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/read_response_text.js +86 -0
- package/node_modules/@clawdbot/lobster/dist/src/read_response_text.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/recipes/github/index.js +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/recipes/github/pr-monitor.js +9 -66
- package/node_modules/@clawdbot/lobster/dist/src/recipes/github/pr-monitor.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/recipes/github/read_pr.js +45 -0
- package/node_modules/@clawdbot/lobster/dist/src/recipes/github/read_pr.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/recipes/github/snapshot.js +45 -0
- package/node_modules/@clawdbot/lobster/dist/src/recipes/github/snapshot.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/recipes/github/stages/pr-view.js +13 -65
- package/node_modules/@clawdbot/lobster/dist/src/recipes/github/stages/pr-view.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/recipes/registry.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/renderers/json.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/resume.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/runtime.js +187 -17
- package/node_modules/@clawdbot/lobster/dist/src/runtime.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/sdk/Lobster.js +25 -134
- package/node_modules/@clawdbot/lobster/dist/src/sdk/Lobster.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/sdk/index.js +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/sdk/primitives/approve.js +1 -2
- package/node_modules/@clawdbot/lobster/dist/src/sdk/primitives/approve.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/sdk/primitives/diff.js +11 -86
- package/node_modules/@clawdbot/lobster/dist/src/sdk/primitives/diff.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/sdk/primitives/exec.js +20 -96
- package/node_modules/@clawdbot/lobster/dist/src/sdk/primitives/exec.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/sdk/primitives/state.js +8 -106
- package/node_modules/@clawdbot/lobster/dist/src/sdk/primitives/state.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/sdk/runtime.js +2 -13
- package/node_modules/@clawdbot/lobster/dist/src/sdk/runtime.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/sdk/state_env.js +6 -0
- package/node_modules/@clawdbot/lobster/dist/src/sdk/state_env.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/shell.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/state/atomic_file.js +192 -0
- package/node_modules/@clawdbot/lobster/dist/src/state/atomic_file.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/state/store.js +550 -136
- package/node_modules/@clawdbot/lobster/dist/src/state/store.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/token.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/validation.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/workflows/dry_run.js +262 -0
- package/node_modules/@clawdbot/lobster/dist/src/workflows/dry_run.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/workflows/expressions.js +345 -0
- package/node_modules/@clawdbot/lobster/dist/src/workflows/expressions.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/workflows/file.js +950 -1285
- package/node_modules/@clawdbot/lobster/dist/src/workflows/file.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/workflows/github_pr_monitor.js +20 -90
- package/node_modules/@clawdbot/lobster/dist/src/workflows/github_pr_monitor.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/workflows/graph.js +9 -8
- package/node_modules/@clawdbot/lobster/dist/src/workflows/graph.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/workflows/load.js +347 -0
- package/node_modules/@clawdbot/lobster/dist/src/workflows/load.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/workflows/registry.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/workflows/types.js +2 -0
- package/node_modules/@clawdbot/lobster/dist/src/workflows/types.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/workflows/values.js +93 -0
- package/node_modules/@clawdbot/lobster/dist/src/workflows/values.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/approval_id.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/approve_preview.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/bin_smoke.test.js +25 -0
- package/node_modules/@clawdbot/lobster/dist/test/bin_smoke.test.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/cancellation.test.js +5171 -0
- package/node_modules/@clawdbot/lobster/dist/test/cancellation.test.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/clawd_invoke.test.js +22 -0
- package/node_modules/@clawdbot/lobster/dist/test/clawd_invoke.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/clawd_invoke_legacy.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/cli_run_errors.test.js +53 -0
- package/node_modules/@clawdbot/lobster/dist/test/cli_run_errors.test.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/cli_run_file_args_json.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/command_arguments.test.js +82 -0
- package/node_modules/@clawdbot/lobster/dist/test/command_arguments.test.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/commands_list.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/condition_comparison.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/core_tool_runtime.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/cost_tracker.test.js +1774 -12
- package/node_modules/@clawdbot/lobster/dist/test/cost_tracker.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/dedupe.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/diff_last.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/doctor.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/dry_run.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/email_triage.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/exec_stdin.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/for_each.test.js +254 -0
- package/node_modules/@clawdbot/lobster/dist/test/for_each.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/github_pr_notify_format.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/github_pr_summary.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/github_pr_view.test.js +176 -0
- package/node_modules/@clawdbot/lobster/dist/test/github_pr_view.test.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/gog_gmail_send.test.js +100 -0
- package/node_modules/@clawdbot/lobster/dist/test/gog_gmail_send.test.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/group_by.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/helpers/node_shell_fixture.js +17 -0
- package/node_modules/@clawdbot/lobster/dist/test/helpers/node_shell_fixture.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/helpers/wait_for_pid.js +19 -0
- package/node_modules/@clawdbot/lobster/dist/test/helpers/wait_for_pid.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/http_response_body_cap.test.js +318 -0
- package/node_modules/@clawdbot/lobster/dist/test/http_response_body_cap.test.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/llm_invoke.test.js +948 -1
- package/node_modules/@clawdbot/lobster/dist/test/llm_invoke.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/llm_task_invoke.test.js +103 -0
- package/node_modules/@clawdbot/lobster/dist/test/llm_task_invoke.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/map.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/multi_approval_resume.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/on_error.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/openclaw_agent.test.js +169 -0
- package/node_modules/@clawdbot/lobster/dist/test/openclaw_agent.test.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/openclaw_dispatch_retry.test.js +96 -0
- package/node_modules/@clawdbot/lobster/dist/test/openclaw_dispatch_retry.test.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/openclaw_invoke_alias.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/parallel.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/parser.test.js +57 -0
- package/node_modules/@clawdbot/lobster/dist/test/parser.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/pipeline_resume_state.test.js +65 -0
- package/node_modules/@clawdbot/lobster/dist/test/pipeline_resume_state.test.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/process_output_limit.test.js +175 -0
- package/node_modules/@clawdbot/lobster/dist/test/process_output_limit.test.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/read_line.test.js +54 -0
- package/node_modules/@clawdbot/lobster/dist/test/read_line.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/release_changelog.test.js +39 -0
- package/node_modules/@clawdbot/lobster/dist/test/release_changelog.test.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/request_input.test.js +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/request_input.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/resume.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/sdk_exec.test.js +248 -0
- package/node_modules/@clawdbot/lobster/dist/test/sdk_exec.test.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/sdk_lobster.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/shell.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/sort.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/state.test.js +584 -1
- package/node_modules/@clawdbot/lobster/dist/test/state.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/step_retry.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/step_stdin_epipe.test.js +44 -0
- package/node_modules/@clawdbot/lobster/dist/test/step_stdin_epipe.test.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/step_timeout.test.js +67 -1
- package/node_modules/@clawdbot/lobster/dist/test/step_timeout.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/template.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/template_filters.test.js +37 -0
- package/node_modules/@clawdbot/lobster/dist/test/template_filters.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/tool_envelope_version.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/tool_mode.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/validation.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/where.test.js +94 -0
- package/node_modules/@clawdbot/lobster/dist/test/where.test.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/workflow_args_env.test.js +5 -3
- package/node_modules/@clawdbot/lobster/dist/test/workflow_args_env.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/workflow_composition.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/workflow_file.test.js +647 -1
- package/node_modules/@clawdbot/lobster/dist/test/workflow_file.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/workflow_graph.test.js +13 -3
- package/node_modules/@clawdbot/lobster/dist/test/workflow_graph.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/workflows.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/package.json +72 -73
- package/node_modules/typebox/build/compile/validator.d.mts +1 -1
- package/node_modules/typebox/build/compile/validator.mjs +1 -1
- package/node_modules/typebox/build/format/idna/idn-hostname.mjs +7 -5
- package/node_modules/typebox/build/format/iri.mjs +5 -1
- package/node_modules/typebox/build/guard/emit.mjs +2 -4
- package/node_modules/typebox/build/guard/guard.mjs +6 -18
- package/node_modules/typebox/build/guard/string.mjs +16 -24
- package/node_modules/typebox/build/schema/build.mjs +2 -2
- package/node_modules/typebox/build/schema/check.mjs +1 -1
- package/node_modules/typebox/build/schema/compile.d.mts +3 -2
- package/node_modules/typebox/build/schema/compile.mjs +8 -8
- package/node_modules/typebox/build/schema/engine/_context.d.mts +6 -7
- package/node_modules/typebox/build/schema/engine/_context.mjs +23 -15
- package/node_modules/typebox/build/schema/engine/_functions.d.mts +2 -2
- package/node_modules/typebox/build/schema/engine/_functions.mjs +1 -1
- package/node_modules/typebox/build/schema/engine/_reducer.d.mts +3 -3
- package/node_modules/typebox/build/schema/engine/_refine.d.mts +5 -5
- package/node_modules/typebox/build/schema/engine/_refine.mjs +4 -7
- package/node_modules/typebox/build/schema/engine/_stack.d.mts +34 -16
- package/node_modules/typebox/build/schema/engine/_stack.mjs +148 -104
- package/node_modules/typebox/build/schema/engine/additionalItems.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/additionalProperties.d.mts +8 -8
- package/node_modules/typebox/build/schema/engine/additionalProperties.mjs +26 -18
- package/node_modules/typebox/build/schema/engine/allOf.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/allOf.mjs +3 -3
- package/node_modules/typebox/build/schema/engine/anyOf.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/anyOf.mjs +5 -10
- package/node_modules/typebox/build/schema/engine/boolean.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/boolean.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/const.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/const.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/contains.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/contains.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/dependencies.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/dependencies.mjs +3 -6
- package/node_modules/typebox/build/schema/engine/dependentRequired.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/dependentRequired.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/dependentSchemas.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/dynamicRef.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/dynamicRef.mjs +11 -7
- package/node_modules/typebox/build/schema/engine/enum.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/enum.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/exclusiveMaximum.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/exclusiveMaximum.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/exclusiveMinimum.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/exclusiveMinimum.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/format.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/format.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/if.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/if.mjs +7 -15
- package/node_modules/typebox/build/schema/engine/items.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/items.mjs +1 -1
- package/node_modules/typebox/build/schema/engine/maxContains.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/maxContains.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/maxItems.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/maxItems.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/maxLength.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/maxLength.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/maxProperties.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/maxProperties.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/maximum.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/maximum.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/minContains.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/minContains.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/minItems.d.mts +5 -5
- package/node_modules/typebox/build/schema/engine/minItems.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/minLength.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/minLength.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/minProperties.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/minProperties.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/minimum.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/minimum.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/multipleOf.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/multipleOf.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/not.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/not.mjs +4 -8
- package/node_modules/typebox/build/schema/engine/oneOf.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/oneOf.mjs +6 -11
- package/node_modules/typebox/build/schema/engine/pattern.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/pattern.mjs +3 -7
- package/node_modules/typebox/build/schema/engine/patternProperties.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/patternProperties.mjs +1 -1
- package/node_modules/typebox/build/schema/engine/prefixItems.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/prefixItems.mjs +1 -1
- package/node_modules/typebox/build/schema/engine/properties.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/properties.mjs +1 -1
- package/node_modules/typebox/build/schema/engine/propertyNames.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/propertyNames.mjs +4 -10
- package/node_modules/typebox/build/schema/engine/recursiveRef.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/recursiveRef.mjs +11 -7
- package/node_modules/typebox/build/schema/engine/ref.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/ref.mjs +19 -15
- package/node_modules/typebox/build/schema/engine/required.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/required.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/schema.d.mts +7 -7
- package/node_modules/typebox/build/schema/engine/schema.mjs +137 -133
- package/node_modules/typebox/build/schema/engine/type.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/type.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/unevaluatedItems.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/unevaluatedItems.mjs +5 -9
- package/node_modules/typebox/build/schema/engine/unevaluatedProperties.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/unevaluatedProperties.mjs +5 -9
- package/node_modules/typebox/build/schema/engine/uniqueItems.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/uniqueItems.mjs +2 -6
- package/node_modules/typebox/build/schema/errors.d.mts +2 -2
- package/node_modules/typebox/build/schema/errors.mjs +8 -14
- package/node_modules/typebox/build/schema/intern/index.d.mts +1 -0
- package/node_modules/typebox/build/schema/intern/index.mjs +1 -0
- package/node_modules/typebox/build/schema/intern/intern.d.mts +11 -0
- package/node_modules/typebox/build/schema/intern/intern.mjs +293 -0
- package/node_modules/typebox/build/schema/meta/draft_2019_09.d.mts +2 -0
- package/node_modules/typebox/build/schema/meta/draft_2019_09.mjs +288 -0
- package/node_modules/typebox/build/schema/meta/draft_2020_12.d.mts +2 -0
- package/node_modules/typebox/build/schema/meta/draft_2020_12.mjs +300 -0
- package/node_modules/typebox/build/schema/meta/draft_3.d.mts +2 -0
- package/node_modules/typebox/build/schema/meta/draft_3.mjs +143 -0
- package/node_modules/typebox/build/schema/meta/draft_4.d.mts +2 -0
- package/node_modules/typebox/build/schema/meta/draft_4.mjs +149 -0
- package/node_modules/typebox/build/schema/meta/draft_6.d.mts +2 -0
- package/node_modules/typebox/build/schema/meta/draft_6.mjs +155 -0
- package/node_modules/typebox/build/schema/meta/draft_7.d.mts +2 -0
- package/node_modules/typebox/build/schema/meta/draft_7.mjs +172 -0
- package/node_modules/typebox/build/schema/meta/index.d.mts +2 -0
- package/node_modules/typebox/build/schema/meta/index.mjs +2 -0
- package/node_modules/typebox/build/schema/meta/meta.d.mts +9 -0
- package/node_modules/typebox/build/schema/meta/meta.mjs +15 -0
- package/node_modules/typebox/build/schema/meta/schema.d.mts +6 -0
- package/node_modules/typebox/build/schema/meta/schema.mjs +1 -0
- package/node_modules/typebox/build/schema/parse.d.mts +1 -0
- package/node_modules/typebox/build/schema/parse.mjs +10 -5
- package/node_modules/typebox/build/schema/resolve/resolve.d.mts +8 -3
- package/node_modules/typebox/build/schema/resolve/resolve.mjs +268 -139
- package/node_modules/typebox/build/schema/schema.d.mts +2 -0
- package/node_modules/typebox/build/schema/schema.mjs +2 -0
- package/node_modules/typebox/build/value/errors/errors.d.mts +2 -12
- package/node_modules/typebox/build/value/errors/errors.mjs +2 -7
- package/node_modules/typebox/package.json +1 -1
- package/node_modules/typebox/readme.md +59 -89
- package/openclaw.plugin.json +1 -1
- package/package.json +10 -7
- package/node_modules/@clawdbot/lobster/dist/src/sdk/token.js +0 -9
- package/node_modules/@clawdbot/lobster/dist/src/sdk/token.js.map +0 -1
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
3
4
|
import http from "node:http";
|
|
4
|
-
import {
|
|
5
|
+
import { promises as fsp } from "node:fs";
|
|
6
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
5
7
|
import { tmpdir } from "node:os";
|
|
6
8
|
import path from "node:path";
|
|
7
9
|
import { createDefaultRegistry } from "../src/commands/registry.js";
|
|
10
|
+
import { keyToPath, stableStringify } from "../src/state/store.js";
|
|
8
11
|
function streamOf(items) {
|
|
9
12
|
return (async function* () {
|
|
10
13
|
for (const item of items)
|
|
@@ -78,6 +81,226 @@ test("llm.invoke auto-detects OpenClaw provider and normalizes output", async ()
|
|
|
78
81
|
await closeServer(server);
|
|
79
82
|
}
|
|
80
83
|
});
|
|
84
|
+
test("llm.invoke re-invokes the adapter when --temperature changes", async () => {
|
|
85
|
+
const registry = createDefaultRegistry();
|
|
86
|
+
const cmd = registry.get("llm.invoke");
|
|
87
|
+
assert.ok(cmd);
|
|
88
|
+
const cacheDir = await mkdtemp(path.join(tmpdir(), "lobster-cache-"));
|
|
89
|
+
const requestLog = [];
|
|
90
|
+
const server = http.createServer((req, res) => {
|
|
91
|
+
if (req.method !== "POST" || req.url !== "/invoke") {
|
|
92
|
+
res.writeHead(404);
|
|
93
|
+
res.end("nope");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
let buf = "";
|
|
97
|
+
req.setEncoding("utf8");
|
|
98
|
+
req.on("data", (d) => (buf += d));
|
|
99
|
+
req.on("end", () => {
|
|
100
|
+
const parsed = JSON.parse(buf || "{}");
|
|
101
|
+
requestLog.push(parsed);
|
|
102
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
103
|
+
res.end(JSON.stringify({
|
|
104
|
+
ok: true,
|
|
105
|
+
result: {
|
|
106
|
+
runId: `pi_${requestLog.length}`,
|
|
107
|
+
model: parsed.model,
|
|
108
|
+
prompt: parsed.prompt,
|
|
109
|
+
output: { format: "text", text: `temperature=${parsed.temperature}` },
|
|
110
|
+
},
|
|
111
|
+
}));
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
115
|
+
const addr = server.address();
|
|
116
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
117
|
+
const ctxEnv = {
|
|
118
|
+
LOBSTER_PI_LLM_ADAPTER_URL: `http://127.0.0.1:${port}`,
|
|
119
|
+
LOBSTER_CACHE_DIR: cacheDir,
|
|
120
|
+
};
|
|
121
|
+
try {
|
|
122
|
+
const runWith = async (temperature) => {
|
|
123
|
+
const result = await cmd.run({
|
|
124
|
+
input: streamOf([]),
|
|
125
|
+
args: {
|
|
126
|
+
_: [],
|
|
127
|
+
provider: "pi",
|
|
128
|
+
model: "test-model",
|
|
129
|
+
prompt: "Sampling parameters matter",
|
|
130
|
+
"schema-version": "v1",
|
|
131
|
+
temperature,
|
|
132
|
+
},
|
|
133
|
+
ctx: baseCtx(ctxEnv, registry),
|
|
134
|
+
});
|
|
135
|
+
return collect(result.output);
|
|
136
|
+
};
|
|
137
|
+
const cold = await runWith(0.1);
|
|
138
|
+
assert.equal(cold[0].source, "pi");
|
|
139
|
+
assert.equal(requestLog.length, 1);
|
|
140
|
+
assert.equal(requestLog[0].temperature, 0.1);
|
|
141
|
+
const changed = await runWith(0.9);
|
|
142
|
+
assert.equal(changed[0].source, "pi");
|
|
143
|
+
assert.equal(requestLog.length, 2);
|
|
144
|
+
assert.equal(requestLog[1].temperature, 0.9);
|
|
145
|
+
assert.notEqual(changed[0].cacheKey, cold[0].cacheKey);
|
|
146
|
+
const replay = await runWith(0.1);
|
|
147
|
+
assert.equal(replay[0].source, "cache");
|
|
148
|
+
assert.equal(replay[0].cacheKey, cold[0].cacheKey);
|
|
149
|
+
assert.equal(requestLog.length, 2);
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
await rm(cacheDir, { recursive: true, force: true });
|
|
153
|
+
await closeServer(server);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
test("llm.invoke re-invokes the adapter when --max-output-tokens changes", async () => {
|
|
157
|
+
const registry = createDefaultRegistry();
|
|
158
|
+
const cmd = registry.get("llm.invoke");
|
|
159
|
+
assert.ok(cmd);
|
|
160
|
+
const cacheDir = await mkdtemp(path.join(tmpdir(), "lobster-cache-"));
|
|
161
|
+
const requestLog = [];
|
|
162
|
+
const server = http.createServer((req, res) => {
|
|
163
|
+
if (req.method !== "POST" || req.url !== "/invoke") {
|
|
164
|
+
res.writeHead(404);
|
|
165
|
+
res.end("nope");
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
let buf = "";
|
|
169
|
+
req.setEncoding("utf8");
|
|
170
|
+
req.on("data", (d) => (buf += d));
|
|
171
|
+
req.on("end", () => {
|
|
172
|
+
const parsed = JSON.parse(buf || "{}");
|
|
173
|
+
requestLog.push(parsed);
|
|
174
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
175
|
+
res.end(JSON.stringify({
|
|
176
|
+
ok: true,
|
|
177
|
+
result: {
|
|
178
|
+
runId: `pi_${requestLog.length}`,
|
|
179
|
+
output: { format: "text", text: `budget=${parsed.maxOutputTokens}` },
|
|
180
|
+
},
|
|
181
|
+
}));
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
185
|
+
const addr = server.address();
|
|
186
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
187
|
+
const ctxEnv = {
|
|
188
|
+
LOBSTER_PI_LLM_ADAPTER_URL: `http://127.0.0.1:${port}`,
|
|
189
|
+
LOBSTER_CACHE_DIR: cacheDir,
|
|
190
|
+
};
|
|
191
|
+
try {
|
|
192
|
+
const runWith = async (maxOutputTokens) => {
|
|
193
|
+
const result = await cmd.run({
|
|
194
|
+
input: streamOf([]),
|
|
195
|
+
args: {
|
|
196
|
+
_: [],
|
|
197
|
+
provider: "pi",
|
|
198
|
+
model: "test-model",
|
|
199
|
+
prompt: "Token budget matters",
|
|
200
|
+
"schema-version": "v1",
|
|
201
|
+
"max-output-tokens": maxOutputTokens,
|
|
202
|
+
},
|
|
203
|
+
ctx: baseCtx(ctxEnv, registry),
|
|
204
|
+
});
|
|
205
|
+
return collect(result.output);
|
|
206
|
+
};
|
|
207
|
+
const cold = await runWith(64);
|
|
208
|
+
assert.equal(cold[0].source, "pi");
|
|
209
|
+
assert.equal(requestLog.length, 1);
|
|
210
|
+
assert.equal(requestLog[0].maxOutputTokens, 64);
|
|
211
|
+
const changed = await runWith(4096);
|
|
212
|
+
assert.equal(changed[0].source, "pi");
|
|
213
|
+
assert.equal(requestLog.length, 2);
|
|
214
|
+
assert.equal(requestLog[1].maxOutputTokens, 4096);
|
|
215
|
+
assert.notEqual(changed[0].cacheKey, cold[0].cacheKey);
|
|
216
|
+
}
|
|
217
|
+
finally {
|
|
218
|
+
await rm(cacheDir, { recursive: true, force: true });
|
|
219
|
+
await closeServer(server);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
test("llm.invoke does not replay a cache entry written before sampling was keyed", async () => {
|
|
223
|
+
const registry = createDefaultRegistry();
|
|
224
|
+
const cmd = registry.get("llm.invoke");
|
|
225
|
+
assert.ok(cmd);
|
|
226
|
+
const cacheDir = await mkdtemp(path.join(tmpdir(), "lobster-cache-"));
|
|
227
|
+
const args = {
|
|
228
|
+
_: [],
|
|
229
|
+
provider: "pi",
|
|
230
|
+
model: "test-model",
|
|
231
|
+
prompt: "Cache key stability",
|
|
232
|
+
"schema-version": "v1",
|
|
233
|
+
};
|
|
234
|
+
// The identity earlier releases hashed: sampling parameters were absent from the payload,
|
|
235
|
+
// so an answer sampled at temperature 0.9 was stored under the same key a request that
|
|
236
|
+
// omits sampling computes. Reading that entry back would serve sampling nobody asked for.
|
|
237
|
+
const legacyKey = createHash("sha256")
|
|
238
|
+
.update(stableStringify({
|
|
239
|
+
provider: "pi",
|
|
240
|
+
prompt: "Cache key stability",
|
|
241
|
+
model: "test-model",
|
|
242
|
+
schemaVersion: "v1",
|
|
243
|
+
artifactHashes: [],
|
|
244
|
+
outputSchema: null,
|
|
245
|
+
}))
|
|
246
|
+
.digest("hex");
|
|
247
|
+
const requestLog = [];
|
|
248
|
+
const server = http.createServer((req, res) => {
|
|
249
|
+
if (req.method !== "POST" || req.url !== "/invoke") {
|
|
250
|
+
res.writeHead(404);
|
|
251
|
+
res.end("nope");
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
let buf = "";
|
|
255
|
+
req.setEncoding("utf8");
|
|
256
|
+
req.on("data", (d) => (buf += d));
|
|
257
|
+
req.on("end", () => {
|
|
258
|
+
requestLog.push(JSON.parse(buf || "{}"));
|
|
259
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
260
|
+
res.end(JSON.stringify({
|
|
261
|
+
ok: true,
|
|
262
|
+
result: { runId: "pi_1", output: { format: "text", text: "fresh" } },
|
|
263
|
+
}));
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
267
|
+
const addr = server.address();
|
|
268
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
269
|
+
try {
|
|
270
|
+
await mkdir(path.join(cacheDir, "llm.invoke"), { recursive: true });
|
|
271
|
+
await writeFile(path.join(cacheDir, "llm.invoke", `${legacyKey}.json`), JSON.stringify({
|
|
272
|
+
cacheKey: legacyKey,
|
|
273
|
+
storedAt: "2026-08-01T00:00:00.000Z",
|
|
274
|
+
items: [
|
|
275
|
+
{
|
|
276
|
+
kind: "llm.invoke",
|
|
277
|
+
cacheKey: legacyKey,
|
|
278
|
+
status: "completed",
|
|
279
|
+
source: "pi",
|
|
280
|
+
cached: false,
|
|
281
|
+
output: { format: "text", text: "sampled at 0.9" },
|
|
282
|
+
},
|
|
283
|
+
],
|
|
284
|
+
}), "utf8");
|
|
285
|
+
const result = await cmd.run({
|
|
286
|
+
input: streamOf([]),
|
|
287
|
+
args,
|
|
288
|
+
ctx: baseCtx({
|
|
289
|
+
LOBSTER_PI_LLM_ADAPTER_URL: `http://127.0.0.1:${port}`,
|
|
290
|
+
LOBSTER_CACHE_DIR: cacheDir,
|
|
291
|
+
}, registry),
|
|
292
|
+
});
|
|
293
|
+
const items = await collect(result.output);
|
|
294
|
+
assert.equal(requestLog.length, 1);
|
|
295
|
+
assert.equal(items[0].source, "pi");
|
|
296
|
+
assert.equal(items[0].output.text, "fresh");
|
|
297
|
+
assert.notEqual(items[0].cacheKey, legacyKey);
|
|
298
|
+
}
|
|
299
|
+
finally {
|
|
300
|
+
await rm(cacheDir, { recursive: true, force: true });
|
|
301
|
+
await closeServer(server);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
81
304
|
test("llm.invoke uses Pi adapter over local HTTP bridge", async () => {
|
|
82
305
|
const registry = createDefaultRegistry();
|
|
83
306
|
const cmd = registry.get("llm.invoke");
|
|
@@ -147,6 +370,693 @@ test("llm.invoke uses Pi adapter over local HTTP bridge", async () => {
|
|
|
147
370
|
await closeServer(server);
|
|
148
371
|
}
|
|
149
372
|
});
|
|
373
|
+
test("llm.invoke aborts the in-flight adapter request when ctx.signal aborts", { timeout: 20_000 }, async () => {
|
|
374
|
+
const registry = createDefaultRegistry();
|
|
375
|
+
const cmd = registry.get("llm.invoke");
|
|
376
|
+
assert.ok(cmd);
|
|
377
|
+
const stalled = createStalledAdapter();
|
|
378
|
+
await stalled.listen();
|
|
379
|
+
const controller = new AbortController();
|
|
380
|
+
try {
|
|
381
|
+
const pending = cmd.run({
|
|
382
|
+
input: streamOf([]),
|
|
383
|
+
args: { _: [], provider: "http", prompt: "Summarize", "disable-cache": true },
|
|
384
|
+
ctx: {
|
|
385
|
+
...baseCtx({ LOBSTER_LLM_ADAPTER_URL: `http://127.0.0.1:${stalled.port}/invoke` }, registry),
|
|
386
|
+
signal: controller.signal,
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
await stalled.requestReceived;
|
|
390
|
+
controller.abort();
|
|
391
|
+
const err = await pending.then(() => null, (e) => e);
|
|
392
|
+
assert.ok(err, "llm.invoke should reject once the run is aborted");
|
|
393
|
+
assert.ok(err.name === "AbortError" || err.code === "ABORT_ERR", `expected an abort error, got ${err.name}: ${err.message}`);
|
|
394
|
+
// The abort must stay recognizable; wrapping it hides the cancellation from
|
|
395
|
+
// workflow timeout and abort handling.
|
|
396
|
+
assert.doesNotMatch(String(err.message), /request failed/);
|
|
397
|
+
await stalled.requestClosed;
|
|
398
|
+
}
|
|
399
|
+
finally {
|
|
400
|
+
controller.abort();
|
|
401
|
+
await stalled.close();
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
test("llm.invoke stops waiting for a direct adapter that ignores ctx.signal", { timeout: 20_000 }, async () => {
|
|
405
|
+
const registry = createDefaultRegistry();
|
|
406
|
+
const cmd = registry.get("llm.invoke");
|
|
407
|
+
assert.ok(cmd);
|
|
408
|
+
let invoked = () => { };
|
|
409
|
+
const adapterInvoked = new Promise((resolve) => (invoked = resolve));
|
|
410
|
+
// A supported ctx.llmAdapters adapter that never resolves and never looks
|
|
411
|
+
// at ctx.signal.
|
|
412
|
+
const stubborn = {
|
|
413
|
+
invoke() {
|
|
414
|
+
invoked();
|
|
415
|
+
return new Promise(() => { });
|
|
416
|
+
},
|
|
417
|
+
};
|
|
418
|
+
const controller = new AbortController();
|
|
419
|
+
const pending = cmd.run({
|
|
420
|
+
input: streamOf([]),
|
|
421
|
+
args: { _: [], provider: "stubborn", prompt: "Summarize", "disable-cache": true },
|
|
422
|
+
ctx: {
|
|
423
|
+
...baseCtx({}, registry),
|
|
424
|
+
llmAdapters: { stubborn },
|
|
425
|
+
signal: controller.signal,
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
await adapterInvoked;
|
|
429
|
+
controller.abort();
|
|
430
|
+
const err = await pending.then(() => null, (e) => e);
|
|
431
|
+
assert.ok(err, "llm.invoke should reject once the run is aborted");
|
|
432
|
+
assert.ok(err.name === "AbortError" || err.code === "ABORT_ERR", `expected an abort error, got ${err.name}: ${err.message}`);
|
|
433
|
+
assert.doesNotMatch(String(err.message), /request failed/);
|
|
434
|
+
});
|
|
435
|
+
test("llm.invoke observes a direct adapter that rejects after aborting the run", async () => {
|
|
436
|
+
const registry = createDefaultRegistry();
|
|
437
|
+
const cmd = registry.get("llm.invoke");
|
|
438
|
+
assert.ok(cmd);
|
|
439
|
+
const controller = new AbortController();
|
|
440
|
+
// An adapter that cancels the run from inside its own invoke and then fails:
|
|
441
|
+
// the shape of an SDK client that tears itself down on a fatal error.
|
|
442
|
+
const selfaborting = {
|
|
443
|
+
invoke() {
|
|
444
|
+
controller.abort();
|
|
445
|
+
return Promise.reject(new Error("adapter tore down its client"));
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
const unhandled = [];
|
|
449
|
+
const onUnhandled = (reason) => unhandled.push(reason);
|
|
450
|
+
process.on("unhandledRejection", onUnhandled);
|
|
451
|
+
try {
|
|
452
|
+
const err = await cmd
|
|
453
|
+
.run({
|
|
454
|
+
input: streamOf([]),
|
|
455
|
+
args: { _: [], provider: "selfaborting", prompt: "Summarize", "disable-cache": true },
|
|
456
|
+
ctx: {
|
|
457
|
+
...baseCtx({}, registry),
|
|
458
|
+
llmAdapters: { selfaborting },
|
|
459
|
+
signal: controller.signal,
|
|
460
|
+
},
|
|
461
|
+
})
|
|
462
|
+
.then(() => null, (e) => e);
|
|
463
|
+
assert.ok(err?.name === "AbortError" || err?.code === "ABORT_ERR", `expected an abort error, got ${err?.name}: ${err?.message}`);
|
|
464
|
+
// The adapter's own rejection must not outlive the cancelled step. Nothing is
|
|
465
|
+
// waiting on it once the abort has won the race, and an unobserved rejection
|
|
466
|
+
// ends the process under Node's default handling.
|
|
467
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
468
|
+
assert.equal(unhandled.length, 0, `adapter rejection went unhandled: ${unhandled[0]?.message ?? unhandled[0]}`);
|
|
469
|
+
}
|
|
470
|
+
finally {
|
|
471
|
+
process.off("unhandledRejection", onUnhandled);
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
test("llm.invoke does not complete from cache when ctx.signal is already aborted", async () => {
|
|
475
|
+
const registry = createDefaultRegistry();
|
|
476
|
+
const cmd = registry.get("llm.invoke");
|
|
477
|
+
assert.ok(cmd);
|
|
478
|
+
const cacheDir = await mkdtemp(path.join(tmpdir(), "lobster-cache-"));
|
|
479
|
+
await mkdir(path.join(cacheDir, "llm.invoke"), { recursive: true });
|
|
480
|
+
let requests = 0;
|
|
481
|
+
const server = http.createServer((req, res) => {
|
|
482
|
+
requests += 1;
|
|
483
|
+
req.resume();
|
|
484
|
+
req.on("end", () => {
|
|
485
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
486
|
+
res.end(JSON.stringify({
|
|
487
|
+
ok: true,
|
|
488
|
+
result: { runId: "cached_1", output: { text: "hello", data: null } },
|
|
489
|
+
}));
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
await new Promise((resolve) => server.listen(0, resolve));
|
|
493
|
+
const addr = server.address();
|
|
494
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
495
|
+
const env = {
|
|
496
|
+
LOBSTER_LLM_ADAPTER_URL: `http://127.0.0.1:${port}/invoke`,
|
|
497
|
+
LOBSTER_CACHE_DIR: cacheDir,
|
|
498
|
+
};
|
|
499
|
+
const args = { _: [], provider: "http", prompt: "Summarize" };
|
|
500
|
+
try {
|
|
501
|
+
// Populate the cache, then repeat the same call on a cancelled run.
|
|
502
|
+
const warm = await cmd.run({
|
|
503
|
+
input: streamOf([]),
|
|
504
|
+
args,
|
|
505
|
+
ctx: baseCtx(env, registry),
|
|
506
|
+
});
|
|
507
|
+
assert.equal((await collect(warm.output))[0].cached, false);
|
|
508
|
+
assert.equal(requests, 1);
|
|
509
|
+
const controller = new AbortController();
|
|
510
|
+
controller.abort();
|
|
511
|
+
await assert.rejects(() => cmd.run({
|
|
512
|
+
input: streamOf([]),
|
|
513
|
+
args,
|
|
514
|
+
ctx: { ...baseCtx(env, registry), signal: controller.signal },
|
|
515
|
+
}), (err) => err?.name === "AbortError" || err?.code === "ABORT_ERR");
|
|
516
|
+
assert.equal(requests, 1, "the cached run must not reach the adapter either");
|
|
517
|
+
}
|
|
518
|
+
finally {
|
|
519
|
+
await rm(cacheDir, { recursive: true, force: true });
|
|
520
|
+
await closeServer(server);
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
test("llm.invoke does not replay a cache hit when ctx.signal aborts while input is drained", async () => {
|
|
524
|
+
const registry = createDefaultRegistry();
|
|
525
|
+
const cmd = registry.get("llm.invoke");
|
|
526
|
+
assert.ok(cmd);
|
|
527
|
+
const cacheDir = await mkdtemp(path.join(tmpdir(), "lobster-cache-"));
|
|
528
|
+
await mkdir(path.join(cacheDir, "llm.invoke"), { recursive: true });
|
|
529
|
+
let requests = 0;
|
|
530
|
+
const server = http.createServer((req, res) => {
|
|
531
|
+
requests += 1;
|
|
532
|
+
req.resume();
|
|
533
|
+
req.on("end", () => {
|
|
534
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
535
|
+
res.end(JSON.stringify({
|
|
536
|
+
ok: true,
|
|
537
|
+
result: { runId: "cached_1", output: { text: "hello", data: null } },
|
|
538
|
+
}));
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
await new Promise((resolve) => server.listen(0, resolve));
|
|
542
|
+
const addr = server.address();
|
|
543
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
544
|
+
const env = {
|
|
545
|
+
LOBSTER_LLM_ADAPTER_URL: `http://127.0.0.1:${port}/invoke`,
|
|
546
|
+
LOBSTER_CACHE_DIR: cacheDir,
|
|
547
|
+
};
|
|
548
|
+
const args = { _: [], provider: "http", prompt: "Summarize" };
|
|
549
|
+
try {
|
|
550
|
+
const warm = await cmd.run({
|
|
551
|
+
input: streamOf([]),
|
|
552
|
+
args,
|
|
553
|
+
ctx: baseCtx(env, registry),
|
|
554
|
+
});
|
|
555
|
+
assert.equal((await collect(warm.output))[0].cached, false);
|
|
556
|
+
assert.equal(requests, 1);
|
|
557
|
+
// The step timeout fires while the upstream step is still producing input,
|
|
558
|
+
// which is after the entry check and before the cache lookup.
|
|
559
|
+
const controller = new AbortController();
|
|
560
|
+
// oxlint-disable-next-line require-yield -- Exercise cancellation before the first item.
|
|
561
|
+
const abortingInput = (async function* () {
|
|
562
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
563
|
+
controller.abort();
|
|
564
|
+
})();
|
|
565
|
+
await assert.rejects(() => cmd.run({
|
|
566
|
+
input: abortingInput,
|
|
567
|
+
args,
|
|
568
|
+
ctx: { ...baseCtx(env, registry), signal: controller.signal },
|
|
569
|
+
}), (err) => err?.name === "AbortError" || err?.code === "ABORT_ERR");
|
|
570
|
+
assert.equal(requests, 1, "a cancelled run must not reach the adapter either");
|
|
571
|
+
}
|
|
572
|
+
finally {
|
|
573
|
+
await rm(cacheDir, { recursive: true, force: true });
|
|
574
|
+
await closeServer(server);
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
test("llm.invoke does not replay run state when ctx.signal aborts while input is drained", async () => {
|
|
578
|
+
const registry = createDefaultRegistry();
|
|
579
|
+
const cmd = registry.get("llm.invoke");
|
|
580
|
+
assert.ok(cmd);
|
|
581
|
+
const stateDir = await mkdtemp(path.join(tmpdir(), "lobster-state-"));
|
|
582
|
+
let requests = 0;
|
|
583
|
+
const server = http.createServer((req, res) => {
|
|
584
|
+
requests += 1;
|
|
585
|
+
req.resume();
|
|
586
|
+
req.on("end", () => {
|
|
587
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
588
|
+
res.end(JSON.stringify({
|
|
589
|
+
ok: true,
|
|
590
|
+
result: { runId: "state_1", output: { text: "hello", data: null } },
|
|
591
|
+
}));
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
await new Promise((resolve) => server.listen(0, resolve));
|
|
595
|
+
const addr = server.address();
|
|
596
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
597
|
+
const env = {
|
|
598
|
+
LOBSTER_LLM_ADAPTER_URL: `http://127.0.0.1:${port}/invoke`,
|
|
599
|
+
LOBSTER_STATE_DIR: stateDir,
|
|
600
|
+
};
|
|
601
|
+
const args = {
|
|
602
|
+
_: [],
|
|
603
|
+
provider: "http",
|
|
604
|
+
prompt: "Summarize",
|
|
605
|
+
"state-key": "step-1",
|
|
606
|
+
"disable-cache": true,
|
|
607
|
+
};
|
|
608
|
+
try {
|
|
609
|
+
const warm = await cmd.run({
|
|
610
|
+
input: streamOf([]),
|
|
611
|
+
args,
|
|
612
|
+
ctx: baseCtx(env, registry),
|
|
613
|
+
});
|
|
614
|
+
assert.equal((await collect(warm.output))[0].cached, false);
|
|
615
|
+
assert.equal(requests, 1);
|
|
616
|
+
const controller = new AbortController();
|
|
617
|
+
// oxlint-disable-next-line require-yield -- Exercise cancellation before the first item.
|
|
618
|
+
const abortingInput = (async function* () {
|
|
619
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
620
|
+
controller.abort();
|
|
621
|
+
})();
|
|
622
|
+
await assert.rejects(() => cmd.run({
|
|
623
|
+
input: abortingInput,
|
|
624
|
+
args,
|
|
625
|
+
ctx: { ...baseCtx(env, registry), signal: controller.signal },
|
|
626
|
+
}), (err) => err?.name === "AbortError" || err?.code === "ABORT_ERR");
|
|
627
|
+
assert.equal(requests, 1, "a cancelled run must not reach the adapter either");
|
|
628
|
+
}
|
|
629
|
+
finally {
|
|
630
|
+
await rm(stateDir, { recursive: true, force: true });
|
|
631
|
+
await closeServer(server);
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
test("llm.invoke does not call the adapter when ctx.signal is already aborted", async () => {
|
|
635
|
+
const registry = createDefaultRegistry();
|
|
636
|
+
const cmd = registry.get("llm.invoke");
|
|
637
|
+
assert.ok(cmd);
|
|
638
|
+
let requests = 0;
|
|
639
|
+
const server = http.createServer((req, res) => {
|
|
640
|
+
requests += 1;
|
|
641
|
+
req.resume();
|
|
642
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
643
|
+
res.end(JSON.stringify({ ok: true, result: { runId: "r1", output: { data: {} } } }));
|
|
644
|
+
});
|
|
645
|
+
await new Promise((resolve) => server.listen(0, resolve));
|
|
646
|
+
const addr = server.address();
|
|
647
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
648
|
+
try {
|
|
649
|
+
const controller = new AbortController();
|
|
650
|
+
controller.abort();
|
|
651
|
+
await assert.rejects(() => cmd.run({
|
|
652
|
+
input: streamOf([]),
|
|
653
|
+
args: { _: [], provider: "http", prompt: "Summarize", "disable-cache": true },
|
|
654
|
+
ctx: {
|
|
655
|
+
...baseCtx({ LOBSTER_LLM_ADAPTER_URL: `http://127.0.0.1:${port}/invoke` }, registry),
|
|
656
|
+
signal: controller.signal,
|
|
657
|
+
},
|
|
658
|
+
}), (err) => err?.name === "AbortError" || err?.code === "ABORT_ERR");
|
|
659
|
+
assert.equal(requests, 0, "an already-cancelled run must not reach the adapter");
|
|
660
|
+
}
|
|
661
|
+
finally {
|
|
662
|
+
await closeServer(server);
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
test("llm.invoke does not retry schema validation after adapter cancellation", async () => {
|
|
666
|
+
const registry = createDefaultRegistry();
|
|
667
|
+
const cmd = registry.get("llm.invoke");
|
|
668
|
+
assert.ok(cmd);
|
|
669
|
+
const controller = new AbortController();
|
|
670
|
+
let calls = 0;
|
|
671
|
+
await assert.rejects(cmd.run({
|
|
672
|
+
input: streamOf([]),
|
|
673
|
+
args: {
|
|
674
|
+
_: [],
|
|
675
|
+
provider: "cancel-test",
|
|
676
|
+
prompt: "Decide",
|
|
677
|
+
"output-schema": '{"type":"object","required":["decision"]}',
|
|
678
|
+
"max-validation-retries": 2,
|
|
679
|
+
},
|
|
680
|
+
ctx: {
|
|
681
|
+
...baseCtx({}, registry),
|
|
682
|
+
signal: controller.signal,
|
|
683
|
+
llmAdapters: {
|
|
684
|
+
"cancel-test": {
|
|
685
|
+
source: "cancel-test",
|
|
686
|
+
async invoke() {
|
|
687
|
+
calls += 1;
|
|
688
|
+
controller.abort(new Error("adapter cancelled during validation"));
|
|
689
|
+
return {
|
|
690
|
+
ok: true,
|
|
691
|
+
result: {
|
|
692
|
+
runId: "cancelled-attempt",
|
|
693
|
+
output: { data: { unexpected: true } },
|
|
694
|
+
},
|
|
695
|
+
};
|
|
696
|
+
},
|
|
697
|
+
},
|
|
698
|
+
},
|
|
699
|
+
},
|
|
700
|
+
}), /adapter cancelled during validation/);
|
|
701
|
+
assert.equal(calls, 1, "cancellation after an invalid response must suppress retries");
|
|
702
|
+
});
|
|
703
|
+
test("llm.invoke aborts while waiting for its reusable run-state lock", async () => {
|
|
704
|
+
const registry = createDefaultRegistry();
|
|
705
|
+
const cmd = registry.get("llm.invoke");
|
|
706
|
+
assert.ok(cmd);
|
|
707
|
+
const cacheDir = await mkdtemp(path.join(tmpdir(), "lobster-llm-state-lock-abort-"));
|
|
708
|
+
const stateDir = path.join(cacheDir, "state");
|
|
709
|
+
const stateKey = "blocked-run-state";
|
|
710
|
+
const lockPath = `${keyToPath(stateDir, stateKey)}.lock`;
|
|
711
|
+
await fsp.mkdir(lockPath, { recursive: true });
|
|
712
|
+
await fsp.writeFile(path.join(lockPath, "owner"), `${process.pid}::live-writer\n`, "utf8");
|
|
713
|
+
try {
|
|
714
|
+
const controller = new AbortController();
|
|
715
|
+
const pending = cmd.run({
|
|
716
|
+
input: streamOf([]),
|
|
717
|
+
args: {
|
|
718
|
+
_: [],
|
|
719
|
+
provider: "state-lock-abort-test",
|
|
720
|
+
prompt: "Decide",
|
|
721
|
+
"state-key": stateKey,
|
|
722
|
+
},
|
|
723
|
+
ctx: {
|
|
724
|
+
...baseCtx({ LOBSTER_CACHE_DIR: cacheDir, LOBSTER_STATE_DIR: stateDir }, registry),
|
|
725
|
+
signal: controller.signal,
|
|
726
|
+
llmAdapters: {
|
|
727
|
+
"state-lock-abort-test": {
|
|
728
|
+
source: "state-lock-abort-test",
|
|
729
|
+
async invoke() {
|
|
730
|
+
throw new Error("adapter must not run while the state read is locked");
|
|
731
|
+
},
|
|
732
|
+
},
|
|
733
|
+
},
|
|
734
|
+
},
|
|
735
|
+
});
|
|
736
|
+
const completion = pending.then(() => ({ kind: "success" }), (error) => ({ kind: "error", error }));
|
|
737
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
738
|
+
controller.abort(new Error("LLM state read cancelled"));
|
|
739
|
+
const early = await Promise.race([
|
|
740
|
+
completion,
|
|
741
|
+
new Promise((resolve) => setTimeout(() => resolve({ kind: "timeout" }), 75)),
|
|
742
|
+
]);
|
|
743
|
+
if (early.kind === "timeout")
|
|
744
|
+
await fsp.rm(lockPath, { recursive: true, force: true });
|
|
745
|
+
const settled = early.kind === "timeout" ? await completion : early;
|
|
746
|
+
assert.notEqual(early.kind, "timeout", "state-key reads must observe cancellation while locked");
|
|
747
|
+
assert.equal(settled.kind, "error");
|
|
748
|
+
if (settled.kind === "error") {
|
|
749
|
+
assert.match(settled.error?.message ?? "", /LLM state read cancelled/);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
finally {
|
|
753
|
+
await fsp.rm(cacheDir, { recursive: true, force: true });
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
test("llm.invoke does not publish a reusable cache entry when cancellation races cache commit", async () => {
|
|
757
|
+
const registry = createDefaultRegistry();
|
|
758
|
+
const cmd = registry.get("llm.invoke");
|
|
759
|
+
assert.ok(cmd);
|
|
760
|
+
const cacheDir = await mkdtemp(path.join(tmpdir(), "lobster-cache-cancel-publication-"));
|
|
761
|
+
const stateDir = path.join(cacheDir, "state");
|
|
762
|
+
const controller = new AbortController();
|
|
763
|
+
const originalRename = fsp.rename;
|
|
764
|
+
let cacheCommitAborted = false;
|
|
765
|
+
let calls = 0;
|
|
766
|
+
const adapter = {
|
|
767
|
+
source: "cache-cancel-test",
|
|
768
|
+
async invoke() {
|
|
769
|
+
calls += 1;
|
|
770
|
+
return {
|
|
771
|
+
ok: true,
|
|
772
|
+
result: {
|
|
773
|
+
runId: `call-${calls}`,
|
|
774
|
+
output: { data: { call: calls } },
|
|
775
|
+
},
|
|
776
|
+
};
|
|
777
|
+
},
|
|
778
|
+
};
|
|
779
|
+
const args = {
|
|
780
|
+
_: [],
|
|
781
|
+
provider: "cache-cancel-test",
|
|
782
|
+
prompt: "Decide",
|
|
783
|
+
"state-key": "cancelled-cache-publication",
|
|
784
|
+
};
|
|
785
|
+
try {
|
|
786
|
+
Object.defineProperty(fsp, "rename", {
|
|
787
|
+
configurable: true,
|
|
788
|
+
writable: true,
|
|
789
|
+
async value(from, to) {
|
|
790
|
+
const result = await originalRename(from, to);
|
|
791
|
+
if (!cacheCommitAborted &&
|
|
792
|
+
String(to).startsWith(`${path.join(cacheDir, "llm.invoke")}${path.sep}`) &&
|
|
793
|
+
String(to).endsWith(".json")) {
|
|
794
|
+
cacheCommitAborted = true;
|
|
795
|
+
controller.abort(new Error("cancelled during cache publication"));
|
|
796
|
+
}
|
|
797
|
+
return result;
|
|
798
|
+
},
|
|
799
|
+
});
|
|
800
|
+
await assert.rejects(cmd.run({
|
|
801
|
+
input: streamOf([]),
|
|
802
|
+
args,
|
|
803
|
+
ctx: {
|
|
804
|
+
...baseCtx({ LOBSTER_CACHE_DIR: cacheDir, LOBSTER_STATE_DIR: stateDir }, registry),
|
|
805
|
+
signal: controller.signal,
|
|
806
|
+
llmAdapters: { "cache-cancel-test": adapter },
|
|
807
|
+
},
|
|
808
|
+
}), /cancelled during cache publication/);
|
|
809
|
+
}
|
|
810
|
+
finally {
|
|
811
|
+
Object.defineProperty(fsp, "rename", {
|
|
812
|
+
configurable: true,
|
|
813
|
+
writable: true,
|
|
814
|
+
value: originalRename,
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
const retried = await cmd.run({
|
|
818
|
+
input: streamOf([]),
|
|
819
|
+
args,
|
|
820
|
+
ctx: {
|
|
821
|
+
...baseCtx({ LOBSTER_CACHE_DIR: cacheDir, LOBSTER_STATE_DIR: stateDir }, registry),
|
|
822
|
+
llmAdapters: { "cache-cancel-test": adapter },
|
|
823
|
+
},
|
|
824
|
+
});
|
|
825
|
+
const retriedItems = await collect(retried.output);
|
|
826
|
+
assert.equal(calls, 2, "a cancelled invocation must not satisfy a later request from cache or run state");
|
|
827
|
+
assert.equal(retriedItems[0]?.source, "cache-cancel-test");
|
|
828
|
+
await rm(cacheDir, { recursive: true, force: true });
|
|
829
|
+
});
|
|
830
|
+
test("llm.invoke restores the previous cache entry when a refresh is cancelled after commit", async () => {
|
|
831
|
+
const registry = createDefaultRegistry();
|
|
832
|
+
const cmd = registry.get("llm.invoke");
|
|
833
|
+
assert.ok(cmd);
|
|
834
|
+
const cacheDir = await mkdtemp(path.join(tmpdir(), "lobster-cache-refresh-cancel-"));
|
|
835
|
+
const stateDir = path.join(cacheDir, "state");
|
|
836
|
+
let calls = 0;
|
|
837
|
+
const adapter = {
|
|
838
|
+
source: "cache-refresh-cancel-test",
|
|
839
|
+
async invoke() {
|
|
840
|
+
calls += 1;
|
|
841
|
+
return {
|
|
842
|
+
ok: true,
|
|
843
|
+
result: {
|
|
844
|
+
runId: `call-${calls}`,
|
|
845
|
+
output: { data: { call: calls } },
|
|
846
|
+
},
|
|
847
|
+
};
|
|
848
|
+
},
|
|
849
|
+
};
|
|
850
|
+
const args = {
|
|
851
|
+
_: [],
|
|
852
|
+
provider: "cache-refresh-cancel-test",
|
|
853
|
+
prompt: "Decide",
|
|
854
|
+
"state-key": "refresh-cache-publication",
|
|
855
|
+
};
|
|
856
|
+
try {
|
|
857
|
+
const first = await cmd.run({
|
|
858
|
+
input: streamOf([]),
|
|
859
|
+
args,
|
|
860
|
+
ctx: {
|
|
861
|
+
...baseCtx({ LOBSTER_CACHE_DIR: cacheDir, LOBSTER_STATE_DIR: stateDir }, registry),
|
|
862
|
+
llmAdapters: { "cache-refresh-cancel-test": adapter },
|
|
863
|
+
},
|
|
864
|
+
});
|
|
865
|
+
assert.deepEqual((await collect(first.output))[0]?.output.data, { call: 1 });
|
|
866
|
+
const controller = new AbortController();
|
|
867
|
+
const originalRename = fsp.rename;
|
|
868
|
+
let cacheCommitAborted = false;
|
|
869
|
+
try {
|
|
870
|
+
Object.defineProperty(fsp, "rename", {
|
|
871
|
+
configurable: true,
|
|
872
|
+
writable: true,
|
|
873
|
+
async value(from, to) {
|
|
874
|
+
const result = await originalRename(from, to);
|
|
875
|
+
if (!cacheCommitAborted &&
|
|
876
|
+
String(to).startsWith(`${path.join(cacheDir, "llm.invoke")}${path.sep}`)) {
|
|
877
|
+
cacheCommitAborted = true;
|
|
878
|
+
controller.abort(new Error("cancelled during cache refresh publication"));
|
|
879
|
+
}
|
|
880
|
+
return result;
|
|
881
|
+
},
|
|
882
|
+
});
|
|
883
|
+
await assert.rejects(cmd.run({
|
|
884
|
+
input: streamOf([]),
|
|
885
|
+
args: { ...args, refresh: true },
|
|
886
|
+
ctx: {
|
|
887
|
+
...baseCtx({ LOBSTER_CACHE_DIR: cacheDir, LOBSTER_STATE_DIR: stateDir }, registry),
|
|
888
|
+
signal: controller.signal,
|
|
889
|
+
llmAdapters: { "cache-refresh-cancel-test": adapter },
|
|
890
|
+
},
|
|
891
|
+
}), /cancelled during cache refresh publication/);
|
|
892
|
+
}
|
|
893
|
+
finally {
|
|
894
|
+
Object.defineProperty(fsp, "rename", {
|
|
895
|
+
configurable: true,
|
|
896
|
+
writable: true,
|
|
897
|
+
value: originalRename,
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
const recovered = await cmd.run({
|
|
901
|
+
input: streamOf([]),
|
|
902
|
+
args,
|
|
903
|
+
ctx: {
|
|
904
|
+
...baseCtx({ LOBSTER_CACHE_DIR: cacheDir, LOBSTER_STATE_DIR: stateDir }, registry),
|
|
905
|
+
llmAdapters: { "cache-refresh-cancel-test": adapter },
|
|
906
|
+
},
|
|
907
|
+
});
|
|
908
|
+
const recoveredItems = await collect(recovered.output);
|
|
909
|
+
assert.equal(calls, 2, "the cancelled refresh must restore the existing run state");
|
|
910
|
+
assert.equal(recoveredItems[0]?.source, "run_state");
|
|
911
|
+
assert.deepEqual(recoveredItems[0]?.output.data, { call: 1 });
|
|
912
|
+
const recoveredCache = await cmd.run({
|
|
913
|
+
input: streamOf([]),
|
|
914
|
+
args: { _: [], provider: "cache-refresh-cancel-test", prompt: "Decide" },
|
|
915
|
+
ctx: {
|
|
916
|
+
...baseCtx({ LOBSTER_CACHE_DIR: cacheDir, LOBSTER_STATE_DIR: stateDir }, registry),
|
|
917
|
+
llmAdapters: { "cache-refresh-cancel-test": adapter },
|
|
918
|
+
},
|
|
919
|
+
});
|
|
920
|
+
const recoveredCacheItems = await collect(recoveredCache.output);
|
|
921
|
+
assert.equal(recoveredCacheItems[0]?.source, "cache");
|
|
922
|
+
assert.deepEqual(recoveredCacheItems[0]?.output.data, { call: 1 });
|
|
923
|
+
}
|
|
924
|
+
finally {
|
|
925
|
+
await rm(cacheDir, { recursive: true, force: true });
|
|
926
|
+
}
|
|
927
|
+
});
|
|
928
|
+
test("llm.invoke rolls back cache and run-state publications after a cache directory sync failure", async () => {
|
|
929
|
+
const registry = createDefaultRegistry();
|
|
930
|
+
const cmd = registry.get("llm.invoke");
|
|
931
|
+
assert.ok(cmd);
|
|
932
|
+
const cacheDir = await mkdtemp(path.join(tmpdir(), "lobster-cache-dir-sync-failure-"));
|
|
933
|
+
const stateDir = path.join(cacheDir, "state");
|
|
934
|
+
const cacheNamespaceDir = path.join(cacheDir, "llm.invoke");
|
|
935
|
+
const originalOpen = fsp.open;
|
|
936
|
+
const fault = Object.assign(new Error("cache directory sync failed"), { code: "EIO" });
|
|
937
|
+
let failNextCacheDirectorySync = true;
|
|
938
|
+
let calls = 0;
|
|
939
|
+
const adapter = {
|
|
940
|
+
source: "cache-dir-sync-test",
|
|
941
|
+
async invoke() {
|
|
942
|
+
calls += 1;
|
|
943
|
+
return { ok: true, result: { runId: `call-${calls}`, output: { data: { call: calls } } } };
|
|
944
|
+
},
|
|
945
|
+
};
|
|
946
|
+
const args = {
|
|
947
|
+
_: [],
|
|
948
|
+
provider: "cache-dir-sync-test",
|
|
949
|
+
prompt: "Decide",
|
|
950
|
+
"state-key": "cache-directory-sync-failure",
|
|
951
|
+
};
|
|
952
|
+
try {
|
|
953
|
+
await fsp.mkdir(cacheNamespaceDir, { recursive: true });
|
|
954
|
+
Object.defineProperty(fsp, "open", {
|
|
955
|
+
configurable: true,
|
|
956
|
+
writable: true,
|
|
957
|
+
async value(...openArgs) {
|
|
958
|
+
const handle = await originalOpen(...openArgs);
|
|
959
|
+
if (failNextCacheDirectorySync &&
|
|
960
|
+
String(openArgs[0]) === cacheNamespaceDir &&
|
|
961
|
+
openArgs[1] === "r") {
|
|
962
|
+
failNextCacheDirectorySync = false;
|
|
963
|
+
return new Proxy(handle, {
|
|
964
|
+
get(target, property, receiver) {
|
|
965
|
+
if (property === "sync")
|
|
966
|
+
return async () => Promise.reject(fault);
|
|
967
|
+
const value = Reflect.get(target, property, receiver);
|
|
968
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
969
|
+
},
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
return handle;
|
|
973
|
+
},
|
|
974
|
+
});
|
|
975
|
+
await assert.rejects(cmd.run({
|
|
976
|
+
input: streamOf([]),
|
|
977
|
+
args,
|
|
978
|
+
ctx: {
|
|
979
|
+
...baseCtx({ LOBSTER_CACHE_DIR: cacheDir, LOBSTER_STATE_DIR: stateDir }, registry),
|
|
980
|
+
llmAdapters: { "cache-dir-sync-test": adapter },
|
|
981
|
+
},
|
|
982
|
+
}), /cache directory sync failed/);
|
|
983
|
+
}
|
|
984
|
+
finally {
|
|
985
|
+
Object.defineProperty(fsp, "open", {
|
|
986
|
+
configurable: true,
|
|
987
|
+
writable: true,
|
|
988
|
+
value: originalOpen,
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
const retried = await cmd.run({
|
|
992
|
+
input: streamOf([]),
|
|
993
|
+
args,
|
|
994
|
+
ctx: {
|
|
995
|
+
...baseCtx({ LOBSTER_CACHE_DIR: cacheDir, LOBSTER_STATE_DIR: stateDir }, registry),
|
|
996
|
+
llmAdapters: { "cache-dir-sync-test": adapter },
|
|
997
|
+
},
|
|
998
|
+
});
|
|
999
|
+
const items = await collect(retried.output);
|
|
1000
|
+
assert.equal(calls, 2, "a failed publication must not be reused from cache or run state");
|
|
1001
|
+
assert.equal(items[0]?.source, "cache-dir-sync-test");
|
|
1002
|
+
await rm(cacheDir, { recursive: true, force: true });
|
|
1003
|
+
});
|
|
1004
|
+
test("llm.invoke reads a populated cache when lock creation is forbidden", async () => {
|
|
1005
|
+
const registry = createDefaultRegistry();
|
|
1006
|
+
const cmd = registry.get("llm.invoke");
|
|
1007
|
+
assert.ok(cmd);
|
|
1008
|
+
const cacheDir = await mkdtemp(path.join(tmpdir(), "lobster-readonly-cache-"));
|
|
1009
|
+
const originalMkdir = fsp.mkdir;
|
|
1010
|
+
let calls = 0;
|
|
1011
|
+
const adapter = {
|
|
1012
|
+
source: "readonly-cache-test",
|
|
1013
|
+
async invoke() {
|
|
1014
|
+
calls += 1;
|
|
1015
|
+
return { ok: true, result: { runId: `call-${calls}`, output: { data: { call: calls } } } };
|
|
1016
|
+
},
|
|
1017
|
+
};
|
|
1018
|
+
const args = { _: [], provider: "readonly-cache-test", prompt: "Decide" };
|
|
1019
|
+
try {
|
|
1020
|
+
const first = await cmd.run({
|
|
1021
|
+
input: streamOf([]),
|
|
1022
|
+
args,
|
|
1023
|
+
ctx: {
|
|
1024
|
+
...baseCtx({ LOBSTER_CACHE_DIR: cacheDir }, registry),
|
|
1025
|
+
llmAdapters: { "readonly-cache-test": adapter },
|
|
1026
|
+
},
|
|
1027
|
+
});
|
|
1028
|
+
await collect(first.output);
|
|
1029
|
+
Object.defineProperty(fsp, "mkdir", {
|
|
1030
|
+
configurable: true,
|
|
1031
|
+
writable: true,
|
|
1032
|
+
async value(filePath, options) {
|
|
1033
|
+
if (String(filePath).endsWith(".lock")) {
|
|
1034
|
+
throw Object.assign(new Error("read-only cache directory"), { code: "EACCES" });
|
|
1035
|
+
}
|
|
1036
|
+
return originalMkdir(filePath, options);
|
|
1037
|
+
},
|
|
1038
|
+
});
|
|
1039
|
+
const cached = await cmd.run({
|
|
1040
|
+
input: streamOf([]),
|
|
1041
|
+
args,
|
|
1042
|
+
ctx: {
|
|
1043
|
+
...baseCtx({ LOBSTER_CACHE_DIR: cacheDir }, registry),
|
|
1044
|
+
llmAdapters: { "readonly-cache-test": adapter },
|
|
1045
|
+
},
|
|
1046
|
+
});
|
|
1047
|
+
const items = await collect(cached.output);
|
|
1048
|
+
assert.equal(calls, 1);
|
|
1049
|
+
assert.equal(items[0]?.source, "cache");
|
|
1050
|
+
}
|
|
1051
|
+
finally {
|
|
1052
|
+
Object.defineProperty(fsp, "mkdir", {
|
|
1053
|
+
configurable: true,
|
|
1054
|
+
writable: true,
|
|
1055
|
+
value: originalMkdir,
|
|
1056
|
+
});
|
|
1057
|
+
await rm(cacheDir, { recursive: true, force: true });
|
|
1058
|
+
}
|
|
1059
|
+
});
|
|
150
1060
|
function baseCtx(envOverrides, registry) {
|
|
151
1061
|
return {
|
|
152
1062
|
stdin: process.stdin,
|
|
@@ -158,6 +1068,43 @@ function baseCtx(envOverrides, registry) {
|
|
|
158
1068
|
render: { json() { }, lines() { } },
|
|
159
1069
|
};
|
|
160
1070
|
}
|
|
1071
|
+
// An adapter that accepts the request and never answers, so the only thing that
|
|
1072
|
+
// can end the call is the caller cancelling it.
|
|
1073
|
+
function createStalledAdapter() {
|
|
1074
|
+
const sockets = new Set();
|
|
1075
|
+
let markReceived = () => { };
|
|
1076
|
+
let markClosed = () => { };
|
|
1077
|
+
const requestReceived = new Promise((resolve) => (markReceived = resolve));
|
|
1078
|
+
const requestClosed = new Promise((resolve) => (markClosed = resolve));
|
|
1079
|
+
const server = http.createServer((req, res) => {
|
|
1080
|
+
req.resume();
|
|
1081
|
+
req.on("end", () => {
|
|
1082
|
+
res.on("close", () => markClosed());
|
|
1083
|
+
markReceived();
|
|
1084
|
+
});
|
|
1085
|
+
});
|
|
1086
|
+
server.on("connection", (socket) => {
|
|
1087
|
+
sockets.add(socket);
|
|
1088
|
+
socket.on("close", () => sockets.delete(socket));
|
|
1089
|
+
});
|
|
1090
|
+
return {
|
|
1091
|
+
requestReceived,
|
|
1092
|
+
requestClosed,
|
|
1093
|
+
port: 0,
|
|
1094
|
+
async listen() {
|
|
1095
|
+
await new Promise((resolve) => server.listen(0, resolve));
|
|
1096
|
+
const addr = server.address();
|
|
1097
|
+
this.port = typeof addr === "object" && addr ? addr.port : 0;
|
|
1098
|
+
},
|
|
1099
|
+
async close() {
|
|
1100
|
+
for (const socket of sockets)
|
|
1101
|
+
socket.destroy();
|
|
1102
|
+
if (!server.listening)
|
|
1103
|
+
return;
|
|
1104
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
1105
|
+
},
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
161
1108
|
async function closeServer(server) {
|
|
162
1109
|
if (!server.listening)
|
|
163
1110
|
return;
|