@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
|
@@ -7,13 +7,27 @@ import { createDefaultRegistry } from "../src/commands/registry.js";
|
|
|
7
7
|
import { runPipeline } from "../src/runtime.js";
|
|
8
8
|
import { diffLast, diffAndStoreValue } from "../src/sdk/primitives/diff.js";
|
|
9
9
|
import { stateSet, readState, writeState } from "../src/sdk/primitives/state.js";
|
|
10
|
-
import { createApprovalIndex, diffAndStore, writeStateJson, readStateJson, writeFileAtomic, writeFileAtomicExclusive, } from "../src/state/store.js";
|
|
10
|
+
import { createApprovalIndex, consumeResumeState, deleteResumeStateWithRollback, diffAndStore, keyToPath, withFileLock, ensureDirectory, stripExtendedLengthPrefix, writeStateJson, readStateJsonWithLock as readStateJson, writeFileAtomic, writeFileAtomicExclusive, } from "../src/state/store.js";
|
|
11
11
|
function streamOf(items) {
|
|
12
12
|
return (async function* () {
|
|
13
13
|
for (const item of items)
|
|
14
14
|
yield item;
|
|
15
15
|
})();
|
|
16
16
|
}
|
|
17
|
+
test("state key boundary trimming preserves normalized filenames", () => {
|
|
18
|
+
const tmp = path.join(os.tmpdir(), "lobster-state");
|
|
19
|
+
for (const [key, filename] of [
|
|
20
|
+
["_Demo KEY_", "demo_key.json"],
|
|
21
|
+
["___Demo KEY___", "demo_key.json"],
|
|
22
|
+
[" Demo KEY ", "demo_key.json"],
|
|
23
|
+
["-Demo KEY-", "-demo_key-.json"],
|
|
24
|
+
[".Demo KEY.", ".demo_key..json"],
|
|
25
|
+
]) {
|
|
26
|
+
assert.equal(keyToPath(tmp, key), path.join(tmp, filename));
|
|
27
|
+
}
|
|
28
|
+
assert.throws(() => keyToPath(tmp, "_"), /state key is empty\/invalid/);
|
|
29
|
+
assert.throws(() => keyToPath(tmp, "___"), /state key is empty\/invalid/);
|
|
30
|
+
});
|
|
17
31
|
test("state.set writes and state.get reads", async () => {
|
|
18
32
|
const tmp = mkdtempSync(path.join(os.tmpdir(), "lobster-state-"));
|
|
19
33
|
const registry = createDefaultRegistry();
|
|
@@ -69,6 +83,49 @@ test("state.get returns null for missing key", async () => {
|
|
|
69
83
|
});
|
|
70
84
|
assert.deepEqual(output.items, [null]);
|
|
71
85
|
});
|
|
86
|
+
test("ordinary state reads work when creating a coordination lock is forbidden", async () => {
|
|
87
|
+
const tmp = mkdtempSync(path.join(os.tmpdir(), "lobster-readonly-state-"));
|
|
88
|
+
const env = { ...process.env, LOBSTER_STATE_DIR: tmp };
|
|
89
|
+
const key = "demo";
|
|
90
|
+
const value = { readable: true };
|
|
91
|
+
const statePath = keyToPath(tmp, key);
|
|
92
|
+
const lockPath = `${statePath}.lock`;
|
|
93
|
+
const originalMkdir = fsp.mkdir;
|
|
94
|
+
await fsp.writeFile(statePath, JSON.stringify(value), "utf8");
|
|
95
|
+
Object.defineProperty(fsp, "mkdir", {
|
|
96
|
+
configurable: true,
|
|
97
|
+
writable: true,
|
|
98
|
+
async value(filePath, options) {
|
|
99
|
+
if (String(filePath) === lockPath) {
|
|
100
|
+
throw Object.assign(new Error("read-only state directory"), { code: "EACCES" });
|
|
101
|
+
}
|
|
102
|
+
return originalMkdir(filePath, options);
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
try {
|
|
106
|
+
assert.deepEqual(await readState(key, { env }), value);
|
|
107
|
+
const registry = createDefaultRegistry();
|
|
108
|
+
const output = await runPipeline({
|
|
109
|
+
pipeline: [{ name: "state.get", args: { _: [key] }, raw: `state.get ${key}` }],
|
|
110
|
+
registry,
|
|
111
|
+
input: [],
|
|
112
|
+
stdin: process.stdin,
|
|
113
|
+
stdout: process.stdout,
|
|
114
|
+
stderr: process.stderr,
|
|
115
|
+
env,
|
|
116
|
+
mode: "tool",
|
|
117
|
+
});
|
|
118
|
+
assert.deepEqual(output.items, [value]);
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
Object.defineProperty(fsp, "mkdir", {
|
|
122
|
+
configurable: true,
|
|
123
|
+
writable: true,
|
|
124
|
+
value: originalMkdir,
|
|
125
|
+
});
|
|
126
|
+
await fsp.rm(tmp, { recursive: true, force: true });
|
|
127
|
+
}
|
|
128
|
+
});
|
|
72
129
|
// --- Atomic-write behavior proofs (issues #108, #109) ---
|
|
73
130
|
//
|
|
74
131
|
// Plain fsp.writeFile truncates the target before writing, so a concurrent
|
|
@@ -153,6 +210,86 @@ test("writeFileAtomic propagates parent directory sync failures", async () => {
|
|
|
153
210
|
const leftovers = (await fsp.readdir(tmp)).filter((f) => f.includes(".tmp"));
|
|
154
211
|
assert.deepEqual(leftovers, []);
|
|
155
212
|
});
|
|
213
|
+
test("consumeResumeState restores a published marker when parent sync fails", async () => {
|
|
214
|
+
const tmp = mkdtempSync(path.join(os.tmpdir(), "lobster-consume-dir-sync-"));
|
|
215
|
+
const env = { LOBSTER_STATE_DIR: tmp };
|
|
216
|
+
const expectedState = { haltType: "approval_request", pipeline: [] };
|
|
217
|
+
const fault = Object.assign(new Error("dir sync failed after resume claim"), { code: "EIO" });
|
|
218
|
+
const originalOpen = fsp.open;
|
|
219
|
+
let failNextDirectorySync = true;
|
|
220
|
+
await writeStateJson({ env, key: "resume", value: expectedState });
|
|
221
|
+
Object.defineProperty(fsp, "open", {
|
|
222
|
+
configurable: true,
|
|
223
|
+
writable: true,
|
|
224
|
+
async value(...args) {
|
|
225
|
+
const handle = await originalOpen(...args);
|
|
226
|
+
if (failNextDirectorySync && String(args[0]) === tmp && args[1] === "r") {
|
|
227
|
+
failNextDirectorySync = false;
|
|
228
|
+
return new Proxy(handle, {
|
|
229
|
+
get(target, property) {
|
|
230
|
+
if (property === "sync")
|
|
231
|
+
return async () => Promise.reject(fault);
|
|
232
|
+
const value = Reflect.get(target, property);
|
|
233
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
return handle;
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
try {
|
|
241
|
+
await assert.rejects(() => consumeResumeState({ env, key: "resume", expectedState }), (err) => err?.code === "EIO");
|
|
242
|
+
assert.deepEqual(await readStateJson({ env, key: "resume" }), expectedState);
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
Object.defineProperty(fsp, "open", {
|
|
246
|
+
configurable: true,
|
|
247
|
+
writable: true,
|
|
248
|
+
value: originalOpen,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
test("deleteResumeStateWithRollback restores a published marker when parent sync fails", async () => {
|
|
253
|
+
const tmp = mkdtempSync(path.join(os.tmpdir(), "lobster-delete-resume-dir-sync-"));
|
|
254
|
+
const env = { LOBSTER_STATE_DIR: tmp };
|
|
255
|
+
const expectedState = { haltType: "input_request", pipeline: [] };
|
|
256
|
+
const fault = Object.assign(new Error("dir sync failed after terminal resume claim"), {
|
|
257
|
+
code: "EIO",
|
|
258
|
+
});
|
|
259
|
+
const originalOpen = fsp.open;
|
|
260
|
+
let failNextDirectorySync = true;
|
|
261
|
+
await writeStateJson({ env, key: "resume", value: expectedState });
|
|
262
|
+
Object.defineProperty(fsp, "open", {
|
|
263
|
+
configurable: true,
|
|
264
|
+
writable: true,
|
|
265
|
+
async value(...args) {
|
|
266
|
+
const handle = await originalOpen(...args);
|
|
267
|
+
if (failNextDirectorySync && String(args[0]) === tmp && args[1] === "r") {
|
|
268
|
+
failNextDirectorySync = false;
|
|
269
|
+
return new Proxy(handle, {
|
|
270
|
+
get(target, property) {
|
|
271
|
+
if (property === "sync")
|
|
272
|
+
return async () => Promise.reject(fault);
|
|
273
|
+
const value = Reflect.get(target, property);
|
|
274
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
return handle;
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
try {
|
|
282
|
+
await assert.rejects(() => deleteResumeStateWithRollback({ env, key: "resume", expectedState }), (err) => err?.code === "EIO");
|
|
283
|
+
assert.deepEqual(await readStateJson({ env, key: "resume" }), expectedState);
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
Object.defineProperty(fsp, "open", {
|
|
287
|
+
configurable: true,
|
|
288
|
+
writable: true,
|
|
289
|
+
value: originalOpen,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
});
|
|
156
293
|
test("readStateJson surfaces malformed authoritative state", async () => {
|
|
157
294
|
const tmp = mkdtempSync(path.join(os.tmpdir(), "lobster-state-corrupt-"));
|
|
158
295
|
const env = { LOBSTER_STATE_DIR: tmp };
|
|
@@ -252,6 +389,431 @@ test("diffAndStore treats corrupt previous state as a miss and rewrites atomical
|
|
|
252
389
|
assert.equal(result.changed, true);
|
|
253
390
|
assert.deepEqual(await readStateJson({ env, key: "snapshot" }), { ok: true });
|
|
254
391
|
});
|
|
392
|
+
test("diffAndStore rolls back a state publication when its parent-directory sync fails", async () => {
|
|
393
|
+
const tmp = mkdtempSync(path.join(os.tmpdir(), "lobster-diff-dir-sync-"));
|
|
394
|
+
const env = { LOBSTER_STATE_DIR: tmp };
|
|
395
|
+
await writeStateJson({ env, key: "snapshot", value: { version: "before" } });
|
|
396
|
+
const fault = Object.assign(new Error("dir sync failed after state publication"), {
|
|
397
|
+
code: "EIO",
|
|
398
|
+
});
|
|
399
|
+
await assert.rejects(() => diffAndStore({
|
|
400
|
+
env,
|
|
401
|
+
key: "snapshot",
|
|
402
|
+
value: { version: "after" },
|
|
403
|
+
atomicWriteOptions: {
|
|
404
|
+
async syncParentDir() {
|
|
405
|
+
throw fault;
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
}), (err) => err?.code === "EIO");
|
|
409
|
+
assert.deepEqual(await readStateJson({ env, key: "snapshot" }), { version: "before" });
|
|
410
|
+
await fsp.rm(tmp, { recursive: true, force: true });
|
|
411
|
+
});
|
|
412
|
+
test("diffAndStore does not publish a snapshot after cancellation before atomic replace", async () => {
|
|
413
|
+
const tmp = mkdtempSync(path.join(os.tmpdir(), "lobster-diff-cancel-publish-"));
|
|
414
|
+
const env = { LOBSTER_STATE_DIR: tmp };
|
|
415
|
+
await writeStateJson({ env, key: "snapshot", value: { version: "before" } });
|
|
416
|
+
const controller = new AbortController();
|
|
417
|
+
const signal = controller.signal;
|
|
418
|
+
const throwIfAborted = signal.throwIfAborted.bind(signal);
|
|
419
|
+
let signalChecks = 0;
|
|
420
|
+
Object.defineProperty(signal, "throwIfAborted", {
|
|
421
|
+
value() {
|
|
422
|
+
signalChecks += 1;
|
|
423
|
+
if (signalChecks === 2)
|
|
424
|
+
controller.abort(new Error("abort before state publish"));
|
|
425
|
+
throwIfAborted();
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
await assert.rejects(() => diffAndStore({ env, key: "snapshot", value: { version: "after" }, signal }), /abort before state publish/);
|
|
429
|
+
assert.equal(signalChecks, 2);
|
|
430
|
+
assert.deepEqual(await readStateJson({ env, key: "snapshot" }), { version: "before" });
|
|
431
|
+
const leftovers = (await fsp.readdir(tmp)).filter((file) => file.includes(".tmp"));
|
|
432
|
+
assert.deepEqual(leftovers, []);
|
|
433
|
+
});
|
|
434
|
+
test("diffAndStore restores the previous snapshot when cancellation arrives during atomic rename", async () => {
|
|
435
|
+
const tmp = mkdtempSync(path.join(os.tmpdir(), "lobster-diff-cancel-rename-"));
|
|
436
|
+
const env = { LOBSTER_STATE_DIR: tmp };
|
|
437
|
+
await writeStateJson({ env, key: "snapshot", value: { version: "before" } });
|
|
438
|
+
const controller = new AbortController();
|
|
439
|
+
await assert.rejects(() => diffAndStore({
|
|
440
|
+
env,
|
|
441
|
+
key: "snapshot",
|
|
442
|
+
value: { version: "after" },
|
|
443
|
+
signal: controller.signal,
|
|
444
|
+
atomicWriteOptions: {
|
|
445
|
+
async renameFile(from, to) {
|
|
446
|
+
await fsp.rename(from, to);
|
|
447
|
+
controller.abort(new Error("abort during atomic rename"));
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
}), /abort during atomic rename/);
|
|
451
|
+
assert.deepEqual(await readStateJson({ env, key: "snapshot" }), { version: "before" });
|
|
452
|
+
const leftovers = (await fsp.readdir(tmp)).filter((file) => file.includes(".tmp"));
|
|
453
|
+
assert.deepEqual(leftovers, []);
|
|
454
|
+
});
|
|
455
|
+
test("diffAndStore removes a newly published snapshot when cancellation arrives during atomic rename", async () => {
|
|
456
|
+
const tmp = mkdtempSync(path.join(os.tmpdir(), "lobster-diff-cancel-new-rename-"));
|
|
457
|
+
const env = { LOBSTER_STATE_DIR: tmp };
|
|
458
|
+
const controller = new AbortController();
|
|
459
|
+
await assert.rejects(() => diffAndStore({
|
|
460
|
+
env,
|
|
461
|
+
key: "snapshot",
|
|
462
|
+
value: { version: "after" },
|
|
463
|
+
signal: controller.signal,
|
|
464
|
+
atomicWriteOptions: {
|
|
465
|
+
async renameFile(from, to) {
|
|
466
|
+
await fsp.rename(from, to);
|
|
467
|
+
controller.abort(new Error("abort during initial atomic rename"));
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
}), /abort during initial atomic rename/);
|
|
471
|
+
assert.equal(await readStateJson({ env, key: "snapshot" }), null);
|
|
472
|
+
const leftovers = (await fsp.readdir(tmp)).filter((file) => file.includes(".tmp"));
|
|
473
|
+
assert.deepEqual(leftovers, []);
|
|
474
|
+
});
|
|
475
|
+
test("diffAndStore restores an existing null snapshot when cancellation arrives during atomic rename", async () => {
|
|
476
|
+
const tmp = mkdtempSync(path.join(os.tmpdir(), "lobster-diff-cancel-null-rename-"));
|
|
477
|
+
const env = { LOBSTER_STATE_DIR: tmp };
|
|
478
|
+
await writeStateJson({ env, key: "snapshot", value: null });
|
|
479
|
+
const snapshotPath = keyToPath(tmp, "snapshot");
|
|
480
|
+
const controller = new AbortController();
|
|
481
|
+
await assert.rejects(() => diffAndStore({
|
|
482
|
+
env,
|
|
483
|
+
key: "snapshot",
|
|
484
|
+
value: { version: "after" },
|
|
485
|
+
signal: controller.signal,
|
|
486
|
+
atomicWriteOptions: {
|
|
487
|
+
async renameFile(from, to) {
|
|
488
|
+
await fsp.rename(from, to);
|
|
489
|
+
controller.abort(new Error("abort during null snapshot rename"));
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
}), /abort during null snapshot rename/);
|
|
493
|
+
assert.equal(await readStateJson({ env, key: "snapshot" }), null);
|
|
494
|
+
assert.equal(await fsp.readFile(snapshotPath, "utf8"), "null\n");
|
|
495
|
+
});
|
|
496
|
+
test("diffAndStore serializes cancellation rollback before a concurrent snapshot update", async () => {
|
|
497
|
+
const tmp = mkdtempSync(path.join(os.tmpdir(), "lobster-diff-cancel-concurrent-"));
|
|
498
|
+
const env = { LOBSTER_STATE_DIR: tmp };
|
|
499
|
+
await writeStateJson({ env, key: "snapshot", value: { version: "before" } });
|
|
500
|
+
const controller = new AbortController();
|
|
501
|
+
let publishCancelledSnapshot;
|
|
502
|
+
const cancelledSnapshotPublished = new Promise((resolve) => {
|
|
503
|
+
publishCancelledSnapshot = resolve;
|
|
504
|
+
});
|
|
505
|
+
let allowCancellation;
|
|
506
|
+
const waitForCancellation = new Promise((resolve) => {
|
|
507
|
+
allowCancellation = resolve;
|
|
508
|
+
});
|
|
509
|
+
const cancelled = diffAndStore({
|
|
510
|
+
env,
|
|
511
|
+
key: "snapshot",
|
|
512
|
+
value: { version: "cancelled-A" },
|
|
513
|
+
signal: controller.signal,
|
|
514
|
+
atomicWriteOptions: {
|
|
515
|
+
async renameFile(from, to) {
|
|
516
|
+
await fsp.rename(from, to);
|
|
517
|
+
publishCancelledSnapshot();
|
|
518
|
+
await waitForCancellation;
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
await cancelledSnapshotPublished;
|
|
523
|
+
let successfulSnapshotPublished = false;
|
|
524
|
+
const successful = writeState("snapshot", { version: "successful-B" }, { env }).then(() => {
|
|
525
|
+
successfulSnapshotPublished = true;
|
|
526
|
+
});
|
|
527
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
528
|
+
assert.equal(successfulSnapshotPublished, false, "the next writer must wait for rollback");
|
|
529
|
+
controller.abort(new Error("abort during concurrent atomic rename"));
|
|
530
|
+
allowCancellation();
|
|
531
|
+
await assert.rejects(cancelled, /abort during concurrent atomic rename/);
|
|
532
|
+
await successful;
|
|
533
|
+
assert.equal(successfulSnapshotPublished, true);
|
|
534
|
+
assert.deepEqual(await readStateJson({ env, key: "snapshot" }), { version: "successful-B" });
|
|
535
|
+
const leftovers = (await fsp.readdir(tmp)).filter((file) => file.includes(".tmp") || file.endsWith(".lock"));
|
|
536
|
+
assert.deepEqual(leftovers, []);
|
|
537
|
+
});
|
|
538
|
+
test("state.get waits for a diff publication to commit or roll back", async () => {
|
|
539
|
+
const tmp = mkdtempSync(path.join(os.tmpdir(), "lobster-state-read-transaction-"));
|
|
540
|
+
const env = { ...process.env, LOBSTER_STATE_DIR: tmp };
|
|
541
|
+
await writeStateJson({ env, key: "snapshot", value: { version: "before" } });
|
|
542
|
+
let markPublished;
|
|
543
|
+
const published = new Promise((resolve) => {
|
|
544
|
+
markPublished = resolve;
|
|
545
|
+
});
|
|
546
|
+
let release;
|
|
547
|
+
const releasePublication = new Promise((resolve) => {
|
|
548
|
+
release = resolve;
|
|
549
|
+
});
|
|
550
|
+
const transaction = diffAndStore({
|
|
551
|
+
env,
|
|
552
|
+
key: "snapshot",
|
|
553
|
+
value: { version: "new" },
|
|
554
|
+
afterStore: async () => {
|
|
555
|
+
markPublished();
|
|
556
|
+
await releasePublication;
|
|
557
|
+
throw new Error("paired publication failed");
|
|
558
|
+
},
|
|
559
|
+
});
|
|
560
|
+
await published;
|
|
561
|
+
const getCmd = createDefaultRegistry().get("state.get");
|
|
562
|
+
const pendingRead = getCmd.run({
|
|
563
|
+
input: streamOf([]),
|
|
564
|
+
args: { _: ["snapshot"] },
|
|
565
|
+
ctx: { stdin: process.stdin, stdout: process.stdout, stderr: process.stderr, env },
|
|
566
|
+
});
|
|
567
|
+
const early = await Promise.race([
|
|
568
|
+
pendingRead.then(() => "settled"),
|
|
569
|
+
new Promise((resolve) => setTimeout(() => resolve("pending"), 25)),
|
|
570
|
+
]);
|
|
571
|
+
assert.equal(early, "pending", "state.get must not expose a snapshot pending rollback");
|
|
572
|
+
release();
|
|
573
|
+
await assert.rejects(transaction, /paired publication failed/);
|
|
574
|
+
const result = await pendingRead;
|
|
575
|
+
const items = [];
|
|
576
|
+
for await (const item of result.output)
|
|
577
|
+
items.push(item);
|
|
578
|
+
assert.deepEqual(items, [{ version: "before" }]);
|
|
579
|
+
await fsp.rm(tmp, { recursive: true, force: true });
|
|
580
|
+
});
|
|
581
|
+
test("state.set stops waiting for a live state lock when its signal is aborted", async () => {
|
|
582
|
+
const tmp = mkdtempSync(path.join(os.tmpdir(), "lobster-state-lock-abort-"));
|
|
583
|
+
const env = { LOBSTER_STATE_DIR: tmp };
|
|
584
|
+
const key = "blocked";
|
|
585
|
+
const lockPath = `${keyToPath(tmp, key)}.lock`;
|
|
586
|
+
await fsp.mkdir(lockPath);
|
|
587
|
+
await fsp.writeFile(path.join(lockPath, "owner"), `${process.pid}::live-writer\n`, "utf8");
|
|
588
|
+
const controller = new AbortController();
|
|
589
|
+
const stateSet = createDefaultRegistry().get("state.set");
|
|
590
|
+
const pending = stateSet.run({
|
|
591
|
+
input: streamOf([{ value: true }]),
|
|
592
|
+
args: { _: [key] },
|
|
593
|
+
ctx: {
|
|
594
|
+
stdin: process.stdin,
|
|
595
|
+
stdout: process.stdout,
|
|
596
|
+
stderr: process.stderr,
|
|
597
|
+
env,
|
|
598
|
+
signal: controller.signal,
|
|
599
|
+
},
|
|
600
|
+
});
|
|
601
|
+
const completion = pending.then(() => ({ kind: "success" }), (error) => ({ kind: "error", error }));
|
|
602
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
603
|
+
controller.abort(new Error("state lock cancelled"));
|
|
604
|
+
const early = await Promise.race([
|
|
605
|
+
completion,
|
|
606
|
+
new Promise((resolve) => setTimeout(() => resolve({ kind: "timeout" }), 75)),
|
|
607
|
+
]);
|
|
608
|
+
if (early.kind === "timeout")
|
|
609
|
+
await fsp.rm(lockPath, { recursive: true, force: true });
|
|
610
|
+
const settled = early.kind === "timeout" ? await completion : early;
|
|
611
|
+
assert.notEqual(early.kind, "timeout", "state.set must not remain blocked after cancellation");
|
|
612
|
+
assert.equal(settled.kind, "error");
|
|
613
|
+
if (settled.kind === "error")
|
|
614
|
+
assert.match(settled.error?.message ?? "", /state lock cancelled/);
|
|
615
|
+
await fsp.rm(lockPath, { recursive: true, force: true });
|
|
616
|
+
});
|
|
617
|
+
test("diffAndStore does not reclaim a live fallback lock after a short heartbeat gap", async () => {
|
|
618
|
+
const tmp = mkdtempSync(path.join(os.tmpdir(), "lobster-state-lock-lease-"));
|
|
619
|
+
const env = { LOBSTER_STATE_DIR: tmp };
|
|
620
|
+
const key = "snapshot";
|
|
621
|
+
const lockPath = `${keyToPath(tmp, key)}.lock`;
|
|
622
|
+
const ownerPath = path.join(lockPath, "owner");
|
|
623
|
+
await fsp.mkdir(lockPath);
|
|
624
|
+
await fsp.writeFile(ownerPath, `${process.pid}::live-writer\n`, "utf8");
|
|
625
|
+
const briefGap = new Date(Date.now() - 2_000);
|
|
626
|
+
await fsp.utimes(lockPath, briefGap, briefGap);
|
|
627
|
+
await fsp.utimes(ownerPath, briefGap, briefGap);
|
|
628
|
+
const controller = new AbortController();
|
|
629
|
+
const abort = setTimeout(() => controller.abort(new Error("live fallback lock remained held")), 100);
|
|
630
|
+
try {
|
|
631
|
+
await assert.rejects(() => diffAndStore({ env, key, value: { version: "new" }, signal: controller.signal }), /live fallback lock remained held/);
|
|
632
|
+
}
|
|
633
|
+
finally {
|
|
634
|
+
clearTimeout(abort);
|
|
635
|
+
await fsp.rm(lockPath, { recursive: true, force: true });
|
|
636
|
+
}
|
|
637
|
+
assert.equal(await readStateJson({ env, key }), null);
|
|
638
|
+
});
|
|
639
|
+
test("diffAndStore reclaims an old lock with a malformed owner", async () => {
|
|
640
|
+
const tmp = mkdtempSync(path.join(os.tmpdir(), "lobster-diff-malformed-lock-"));
|
|
641
|
+
const env = { LOBSTER_STATE_DIR: tmp };
|
|
642
|
+
const lockPath = `${keyToPath(tmp, "snapshot")}.lock`;
|
|
643
|
+
await fsp.mkdir(lockPath);
|
|
644
|
+
await fsp.writeFile(path.join(lockPath, "owner"), "\n", "utf8");
|
|
645
|
+
const staleAt = new Date(Date.now() - 10_000);
|
|
646
|
+
await fsp.utimes(lockPath, staleAt, staleAt);
|
|
647
|
+
await diffAndStore({ env, key: "snapshot", value: { version: "recovered" } });
|
|
648
|
+
assert.deepEqual(await readStateJson({ env, key: "snapshot" }), { version: "recovered" });
|
|
649
|
+
await assert.rejects(fsp.access(lockPath));
|
|
650
|
+
});
|
|
651
|
+
test("diffAndStore reclaims an old lock after its owner PID is reused", { skip: process.platform !== "linux" }, async () => {
|
|
652
|
+
const tmp = mkdtempSync(path.join(os.tmpdir(), "lobster-diff-reused-pid-lock-"));
|
|
653
|
+
const env = { LOBSTER_STATE_DIR: tmp };
|
|
654
|
+
const lockPath = `${keyToPath(tmp, "snapshot")}.lock`;
|
|
655
|
+
const ownerPath = path.join(lockPath, "owner");
|
|
656
|
+
await fsp.mkdir(lockPath);
|
|
657
|
+
await fsp.writeFile(ownerPath, `${process.pid}:0:stale-owner\n`, "utf8");
|
|
658
|
+
const staleAt = new Date(Date.now() - 10_000);
|
|
659
|
+
await fsp.utimes(lockPath, staleAt, staleAt);
|
|
660
|
+
await fsp.utimes(ownerPath, staleAt, staleAt);
|
|
661
|
+
const controller = new AbortController();
|
|
662
|
+
const timeout = setTimeout(() => controller.abort(new Error("reused lock was not reclaimed")), 250);
|
|
663
|
+
try {
|
|
664
|
+
await diffAndStore({
|
|
665
|
+
env,
|
|
666
|
+
key: "snapshot",
|
|
667
|
+
value: { version: "recovered" },
|
|
668
|
+
signal: controller.signal,
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
finally {
|
|
672
|
+
clearTimeout(timeout);
|
|
673
|
+
}
|
|
674
|
+
assert.equal(controller.signal.aborted, false);
|
|
675
|
+
assert.deepEqual(await readStateJson({ env, key: "snapshot" }), { version: "recovered" });
|
|
676
|
+
await assert.rejects(fsp.access(lockPath));
|
|
677
|
+
});
|
|
678
|
+
test("withFileLock does not reclaim a replacement lock when its inode is reused", async () => {
|
|
679
|
+
const tmp = mkdtempSync(path.join(os.tmpdir(), "lobster-state-lock-replacement-"));
|
|
680
|
+
const filePath = path.join(tmp, "snapshot.json");
|
|
681
|
+
const lockPath = `${filePath}.lock`;
|
|
682
|
+
await fsp.mkdir(lockPath);
|
|
683
|
+
await fsp.writeFile(path.join(lockPath, "owner"), "2147483647::stale-owner\n", "utf8");
|
|
684
|
+
const staleAt = new Date(Date.now() - 10_000);
|
|
685
|
+
await fsp.utimes(lockPath, staleAt, staleAt);
|
|
686
|
+
await fsp.utimes(path.join(lockPath, "owner"), staleAt, staleAt);
|
|
687
|
+
const originalReadFile = fsp.readFile;
|
|
688
|
+
const originalLstat = fsp.lstat;
|
|
689
|
+
const staleLock = await originalLstat(lockPath);
|
|
690
|
+
let replaced = false;
|
|
691
|
+
let replacementActive = false;
|
|
692
|
+
let overlap = false;
|
|
693
|
+
let original;
|
|
694
|
+
let replacement;
|
|
695
|
+
let releaseReplacement;
|
|
696
|
+
const replacementReleased = new Promise((resolve) => {
|
|
697
|
+
releaseReplacement = resolve;
|
|
698
|
+
});
|
|
699
|
+
let replacementStarted;
|
|
700
|
+
const replacementEntered = new Promise((resolve) => {
|
|
701
|
+
replacementStarted = resolve;
|
|
702
|
+
});
|
|
703
|
+
let markReplacementObserved;
|
|
704
|
+
const replacementObserved = new Promise((resolve) => {
|
|
705
|
+
markReplacementObserved = resolve;
|
|
706
|
+
});
|
|
707
|
+
Object.defineProperty(fsp, "lstat", {
|
|
708
|
+
configurable: true,
|
|
709
|
+
writable: true,
|
|
710
|
+
async value(...args) {
|
|
711
|
+
const stat = await originalLstat(...args);
|
|
712
|
+
if (replaced && String(args[0]) === lockPath) {
|
|
713
|
+
// Linux can recycle a deleted directory's inode for its replacement.
|
|
714
|
+
Object.assign(stat, { dev: staleLock.dev, ino: staleLock.ino });
|
|
715
|
+
}
|
|
716
|
+
return stat;
|
|
717
|
+
},
|
|
718
|
+
});
|
|
719
|
+
Object.defineProperty(fsp, "readFile", {
|
|
720
|
+
configurable: true,
|
|
721
|
+
writable: true,
|
|
722
|
+
async value(filePathArg, options) {
|
|
723
|
+
const result = await originalReadFile(filePathArg, options);
|
|
724
|
+
if (!replaced && String(filePathArg) === path.join(lockPath, "owner")) {
|
|
725
|
+
replaced = true;
|
|
726
|
+
await fsp.rm(lockPath, { recursive: true, force: true });
|
|
727
|
+
replacement = withFileLock({
|
|
728
|
+
filePath,
|
|
729
|
+
task: async () => {
|
|
730
|
+
replacementActive = true;
|
|
731
|
+
replacementStarted();
|
|
732
|
+
await replacementReleased;
|
|
733
|
+
replacementActive = false;
|
|
734
|
+
},
|
|
735
|
+
});
|
|
736
|
+
await replacementEntered;
|
|
737
|
+
}
|
|
738
|
+
else if (replacementActive && String(filePathArg) === path.join(lockPath, "owner")) {
|
|
739
|
+
markReplacementObserved();
|
|
740
|
+
}
|
|
741
|
+
return result;
|
|
742
|
+
},
|
|
743
|
+
});
|
|
744
|
+
try {
|
|
745
|
+
original = withFileLock({
|
|
746
|
+
filePath,
|
|
747
|
+
task: async () => {
|
|
748
|
+
if (replacementActive)
|
|
749
|
+
overlap = true;
|
|
750
|
+
},
|
|
751
|
+
});
|
|
752
|
+
await replacementEntered;
|
|
753
|
+
await Promise.race([original, replacementObserved]);
|
|
754
|
+
assert.equal(overlap, false, "the stale reclaimer must not enter beside the replacement");
|
|
755
|
+
releaseReplacement();
|
|
756
|
+
if (!replacement)
|
|
757
|
+
throw new Error("replacement lock did not start");
|
|
758
|
+
await Promise.all([original, replacement]);
|
|
759
|
+
}
|
|
760
|
+
finally {
|
|
761
|
+
releaseReplacement();
|
|
762
|
+
await Promise.allSettled([original, replacement]);
|
|
763
|
+
Object.defineProperty(fsp, "lstat", {
|
|
764
|
+
configurable: true,
|
|
765
|
+
writable: true,
|
|
766
|
+
value: originalLstat,
|
|
767
|
+
});
|
|
768
|
+
Object.defineProperty(fsp, "readFile", {
|
|
769
|
+
configurable: true,
|
|
770
|
+
writable: true,
|
|
771
|
+
value: originalReadFile,
|
|
772
|
+
});
|
|
773
|
+
await fsp.rm(tmp, { recursive: true, force: true });
|
|
774
|
+
}
|
|
775
|
+
assert.equal(replaced, true);
|
|
776
|
+
assert.equal(overlap, false);
|
|
777
|
+
});
|
|
778
|
+
test("withFileLock releases its key when best-effort cleanup cannot remove the detached lock", async () => {
|
|
779
|
+
const tmp = mkdtempSync(path.join(os.tmpdir(), "lobster-state-lock-release-failure-"));
|
|
780
|
+
const filePath = path.join(tmp, "snapshot.json");
|
|
781
|
+
const lockPath = `${filePath}.lock`;
|
|
782
|
+
const originalRm = fsp.rm;
|
|
783
|
+
let failReleaseOnce = true;
|
|
784
|
+
Object.defineProperty(fsp, "rm", {
|
|
785
|
+
configurable: true,
|
|
786
|
+
writable: true,
|
|
787
|
+
async value(filePathArg, options) {
|
|
788
|
+
if (failReleaseOnce && String(filePathArg).startsWith(lockPath)) {
|
|
789
|
+
failReleaseOnce = false;
|
|
790
|
+
throw Object.assign(new Error("simulated lock cleanup failure"), { code: "EIO" });
|
|
791
|
+
}
|
|
792
|
+
return originalRm(filePathArg, options);
|
|
793
|
+
},
|
|
794
|
+
});
|
|
795
|
+
let second;
|
|
796
|
+
try {
|
|
797
|
+
await withFileLock({ filePath, task: async () => { } });
|
|
798
|
+
second = withFileLock({ filePath, task: async () => "reacquired" });
|
|
799
|
+
const settled = await Promise.race([
|
|
800
|
+
second,
|
|
801
|
+
new Promise((resolve) => setTimeout(() => resolve("timed out"), 500)),
|
|
802
|
+
]);
|
|
803
|
+
if (settled === "timed out")
|
|
804
|
+
await originalRm(lockPath, { recursive: true, force: true });
|
|
805
|
+
assert.equal(settled, "reacquired", "a failed cleanup must not poison the live state lock");
|
|
806
|
+
await second;
|
|
807
|
+
}
|
|
808
|
+
finally {
|
|
809
|
+
Object.defineProperty(fsp, "rm", {
|
|
810
|
+
configurable: true,
|
|
811
|
+
writable: true,
|
|
812
|
+
value: originalRm,
|
|
813
|
+
});
|
|
814
|
+
await originalRm(tmp, { recursive: true, force: true });
|
|
815
|
+
}
|
|
816
|
+
});
|
|
255
817
|
test("SDK diff primitives treat corrupt previous state as a miss (#112)", async () => {
|
|
256
818
|
const tmp = mkdtempSync(path.join(os.tmpdir(), "lobster-sdk-diff-corrupt-"));
|
|
257
819
|
const ctx = { env: { LOBSTER_STATE_DIR: tmp } };
|
|
@@ -333,4 +895,25 @@ test("SDK writeState removes temp files when replacement fails", async () => {
|
|
|
333
895
|
const leftovers = (await fsp.readdir(tmp)).filter((f) => f.includes(".tmp"));
|
|
334
896
|
assert.deepEqual(leftovers, []);
|
|
335
897
|
});
|
|
898
|
+
test("ensureDirectory creates missing parent directories", async () => {
|
|
899
|
+
const tmp = mkdtempSync(path.join(os.tmpdir(), "lobster-ensure-dir-"));
|
|
900
|
+
const nested = path.join(tmp, "alpha", "beta", "gamma");
|
|
901
|
+
await ensureDirectory(nested);
|
|
902
|
+
assert.equal((await fsp.stat(nested)).isDirectory(), true);
|
|
903
|
+
// Re-running must stay a no-op once the whole chain already exists.
|
|
904
|
+
await ensureDirectory(nested);
|
|
905
|
+
assert.equal((await fsp.stat(nested)).isDirectory(), true);
|
|
906
|
+
});
|
|
907
|
+
test("stripExtendedLengthPrefix maps only namespaces with a plain equivalent", () => {
|
|
908
|
+
assert.equal(stripExtendedLengthPrefix("\\\\?\\C:\\lobster\\state"), "C:\\lobster\\state");
|
|
909
|
+
assert.equal(stripExtendedLengthPrefix("\\\\?\\UNC\\server\\share\\state"), "\\\\server\\share\\state");
|
|
910
|
+
// Windows matches the namespace component case-insensitively, so a lowercase
|
|
911
|
+
// marker names the same share and must map to the same plain path.
|
|
912
|
+
assert.equal(stripExtendedLengthPrefix("\\\\?\\unc\\server\\share\\state"), "\\\\server\\share\\state");
|
|
913
|
+
// A device namespace has no drive-letter form, so stripping it would leave a
|
|
914
|
+
// relative path and break an explicitly configured state directory.
|
|
915
|
+
const volume = "\\\\?\\Volume{6f4c2b1a-0000-0000-0000-000000000000}\\lobster\\state";
|
|
916
|
+
assert.equal(stripExtendedLengthPrefix(volume), volume);
|
|
917
|
+
assert.equal(stripExtendedLengthPrefix("/tmp/lobster/state"), "/tmp/lobster/state");
|
|
918
|
+
});
|
|
336
919
|
//# sourceMappingURL=state.test.js.map
|