blq-cli 1.0.0__tar.gz → 1.0.2__tar.gz
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.
- blq_cli-1.0.2/CHANGELOG.md +6 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/PKG-INFO +3 -3
- blq_cli-1.0.2/docs/plans/explore-bird-sqlite-backend.md +105 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/pyproject.toml +3 -3
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/__init__.py +1 -1
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/bird.py +112 -40
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/cli.py +2 -6
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/commands/core.py +1 -2
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/commands/events.py +4 -1
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/commands/execution.py +28 -26
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/commands/init_cmd.py +10 -2
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/commands/mcp_cmd.py +61 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/commands/sandbox_cmd.py +5 -5
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/commands/sync_cmd.py +4 -1
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/ext/__init__.py +1 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/ext/annotator.py +5 -8
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/ext/discovery.py +1 -1
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/ext/local_executor.py +6 -5
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/ext/pipeline.py +3 -5
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/query.py +1 -1
- blq_cli-1.0.2/src/blq/runtime.py +152 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/serve.py +77 -8
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/services/__init__.py +1 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/services/query.py +30 -30
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/services/refs.py +3 -12
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/storage.py +1 -3
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq_sandbox/__init__.py +4 -3
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq_sandbox/engines.py +4 -7
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq_sandbox/profile.py +21 -20
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq_sandbox/source_annotator.py +16 -15
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq_sandbox/spec.py +0 -1
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq_sandbox/strace_parser.py +11 -16
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq_sandbox/tighten.py +2 -2
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq_sandbox/violations.py +13 -4
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq_sandbox_bwrap/__init__.py +2 -3
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq_sandbox_systemd/__init__.py +3 -7
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_annotator.py +19 -11
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_bird.py +1 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_bwrap_args.py +0 -3
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_bwrap_engine.py +17 -18
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_command_lock_field.py +1 -0
- blq_cli-1.0.2/tests/test_config_consumption.py +84 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_execution_locks.py +1 -1
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_ext_discovery.py +2 -1
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_ext_integration.py +33 -22
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_ext_local_executor.py +1 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_ext_pipeline.py +9 -4
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_ext_types.py +2 -1
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_locks.py +8 -2
- blq_cli-1.0.2/tests/test_runtime_config.py +175 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_sandbox.py +8 -4
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_sandbox_cmd.py +1 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_sandbox_events.py +47 -42
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_sandbox_ext.py +6 -3
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_sandbox_profile.py +1 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_sandbox_register.py +1 -2
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_sandbox_systemd.py +6 -12
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_sandbox_tighten.py +1 -2
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_sandbox_violations.py +1 -2
- blq_cli-1.0.2/tests/test_schema_repair.py +95 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_services_execution.py +0 -3
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_services_inspect.py +2 -5
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_services_query.py +21 -7
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_source_annotator.py +12 -21
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_strace_parser.py +2 -2
- blq_cli-1.0.2/uv.lock +3011 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/.claude/hooks/blq-suggest.sh +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/.github/workflows/ci.yml +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/.github/workflows/docs.yml +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/.github/workflows/publish.yml +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/.gitignore +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/.mcp.json +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/.readthedocs.yml +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/AGENT.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/AGENTS.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/CLAUDE.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/CONTRIBUTING.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/README.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/SKILL.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/api-reference.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/commands/capture.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/commands/ci.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/commands/completions.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/commands/errors.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/commands/exec.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/commands/filter.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/commands/index.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/commands/init.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/commands/maintenance.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/commands/query.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/commands/registry.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/commands/report.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/commands/run.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/commands/sql.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/commands/status.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/commands/sync.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/commands/watch.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/design/design-bird-migration.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/design/design-commands.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/design/design-config-command.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/design/design-extensions.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/design/design-git-integration.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/design/design-hooks-v2.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/design/design-live-inspection.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/design/design-parameterized-commands.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/design/design-run-args.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/design/design-sandbox-specs.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/design/design-sync.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/design/design-track-save.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/design/duck-hunt-v2-migration.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/design/duck-hunt-v3-migration.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/design/proposal-bird-v5.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/design/skill-inspect-enrichment.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/getting-started.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/index.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/integration.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/mcp.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/plans/explore-nsjail-python-wrapper.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/plans/explore-nsjail-spack-package.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/plans/patterns-integration-prompt.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/plans/roadmap-to-1.0.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/plugin-guide.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/python-api.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/query-guide.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/requirements.txt +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/sandbox.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/schema-stability.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/schema.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/superpowers/plans/2026-03-22-extension-system.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/superpowers/plans/2026-03-28-bwrap-engine.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/superpowers/plans/2026-03-28-command-locks.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/superpowers/plans/2026-03-29-annotators-and-tighten.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/superpowers/plans/2026-03-29-service-layer-phase1.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/superpowers/plans/2026-03-29-strace-profiling.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/superpowers/specs/2026-03-22-extension-system-design.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/docs/superpowers/specs/2026-03-29-unified-service-layer-design.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/experiments/agent-build-test.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/mkdocs.yml +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/__main__.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/bird_schema.sql +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/commands/README.md +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/commands/__init__.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/commands/ci_cmd.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/commands/clean_cmd.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/commands/config_cmd.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/commands/hooks_cmd.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/commands/hooks_gen.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/commands/management.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/commands/management_cmd.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/commands/migrate.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/commands/query_cmd.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/commands/record_cmd.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/commands/registry.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/commands/report_cmd.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/commands/serve_cmd.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/commands/watch_cmd.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/config_format.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/git.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/github.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/locks.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/output.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/schema.sql +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/services/execution.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/services/inspect.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/templates/drone.yml.j2 +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/templates/git_hook.sh.j2 +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/templates/github_workflow.yml.j2 +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/templates/gitlab_ci.yml.j2 +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/templates/hook_script.sh.j2 +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq/user_config.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/src/blq_sandbox_bwrap/args.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/__init__.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/conftest.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/sql/test_duck_hunt_v2_migration.sql +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_attempts_outcomes.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_auto_init.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_autoprune.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_ci.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_ci_generators.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_command_args.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_core.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_exit_code_reason.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_git.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_hooks.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_hooks_gen.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_inspect.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_json_null_filter.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_mcp_ci_tools.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_mcp_merge.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_mcp_server.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_phase1_structured_output.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_phase2_command_registry.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_query_api.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_query_filter.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_record_invocation.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_report.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_services_refs.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_storage.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_storage_prune.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_user_config.py +0 -0
- {blq_cli-1.0.0 → blq_cli-1.0.2}/tests/test_watch.py +0 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## v1.0.2
|
|
4
|
+
|
|
5
|
+
### Bug fixes
|
|
6
|
+
- **Self-heal DBs stuck without `extension_data`.** The 2.3->2.4 migration renamed `sandbox` -> `extension_data` via `RENAME COLUMN`, which DuckDB blocks when non-view dependencies (FKs/constraints) exist on `attempts`/`invocations` — even after dropping views. The rename failed silently but the schema version advanced anyway, leaving DBs permanently stuck without `extension_data` and crashing every `write_attempt` ("Table attempts does not have a column extension_data"). `_ensure_schema` now self-heals version-independently: it gates on a missing column, adds `extension_data` via `ADD COLUMN` (never blocked), copies any existing `sandbox` data across (wrapped as `{"sandbox": ...}`, no data loss), and re-applies the idempotent schema to recreate views. Healthy/fresh DBs stay a fast no-op. Repairs on next connect (the CLI heals on the next `blq run`; a running MCP server heals after restart).
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: blq-cli
|
|
3
|
-
Version: 1.0.
|
|
4
|
-
Summary:
|
|
5
|
-
Project-URL: Homepage, https://blq-cli.readthedocs.
|
|
3
|
+
Version: 1.0.2
|
|
4
|
+
Summary: Build Log Query - capture and query build/test logs with DuckDB
|
|
5
|
+
Project-URL: Homepage, https://blq-cli.readthedocs.io/
|
|
6
6
|
Project-URL: Repository, https://github.com/teaguesterling/blq-cli
|
|
7
7
|
Project-URL: Issues, https://github.com/teaguesterling/blq-cli/issues
|
|
8
8
|
Author-email: Teague Sterling <teaguesterling@gmail.com>
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Exploration: SQLite as BIRD OLTP Backend
|
|
2
|
+
|
|
3
|
+
*A prompt for exploring SQLite as the transactional write layer for BIRD, with DuckDB for analytics.*
|
|
4
|
+
|
|
5
|
+
## Context
|
|
6
|
+
|
|
7
|
+
The BIRD spec v5 (at `~/Projects/magic/docs/spec/v5/bird-v5.md`) defines a layered architecture:
|
|
8
|
+
- **D-layers** (D0-D4): logical query interface (attempts, outcomes, events, etc.)
|
|
9
|
+
- **S-layers**: storage implementation (currently parquet files or DuckDB tables)
|
|
10
|
+
|
|
11
|
+
blq currently uses DuckDB tables for both OLTP (writes) and OLAP (reads), with a retry/backoff pattern to handle DuckDB's single-writer limitation.
|
|
12
|
+
|
|
13
|
+
Kibitzer (`~/Projects/kibitzer`) uses JSON state files for high-frequency writes (every tool call) and wants to share data with blq.
|
|
14
|
+
|
|
15
|
+
## The Proposal
|
|
16
|
+
|
|
17
|
+
Add SQLite as an S-layer option for BIRD — specifically for the OLTP (write) path:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
Tool hooks (kibitzer) → .bird/bird.sqlite ← blq run
|
|
21
|
+
MCP servers → (OLTP writes) ← blq exec
|
|
22
|
+
↓
|
|
23
|
+
DuckDB ATTACH
|
|
24
|
+
↓
|
|
25
|
+
.bird/blq.duckdb
|
|
26
|
+
(OLAP reads)
|
|
27
|
+
↓
|
|
28
|
+
blq events / blq sql / MCP queries
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Why SQLite for writes
|
|
32
|
+
- WAL mode handles concurrent writes gracefully (vs DuckDB single-writer)
|
|
33
|
+
- Designed for embedded OLTP (vs DuckDB designed for OLAP)
|
|
34
|
+
- Kibitzer writes on *every tool call* — needs fast, concurrent inserts
|
|
35
|
+
- DuckDB's `sqlite` extension can ATTACH and read SQLite tables natively
|
|
36
|
+
- Eliminates the retry/backoff dance in blq's Window 1/Window 2 pattern
|
|
37
|
+
|
|
38
|
+
### Why keep DuckDB for reads
|
|
39
|
+
- OLAP queries (aggregations, joins, window functions) are DuckDB's strength
|
|
40
|
+
- Macros like `blq_load_events()` use DuckDB-specific SQL
|
|
41
|
+
- duck_hunt parsing runs inside DuckDB
|
|
42
|
+
- DuckDB's columnar storage is better for analytics over large datasets
|
|
43
|
+
|
|
44
|
+
## Questions to Explore
|
|
45
|
+
|
|
46
|
+
### 1. BIRD Spec Changes
|
|
47
|
+
|
|
48
|
+
Read the full BIRD v5 spec at `~/Projects/magic/docs/spec/v5/bird-v5.md`.
|
|
49
|
+
|
|
50
|
+
- How does the S-layer abstraction work? Can SQLite be added as an S-layer without changing D-layers?
|
|
51
|
+
- Should SQLite replace parquet as the default, or be a third option?
|
|
52
|
+
- How does DuckDB's `ATTACH 'bird.sqlite' AS bird_sqlite (TYPE SQLITE)` interact with BIRD's schema ownership model?
|
|
53
|
+
- Should the `invocations` view join across the DuckDB/SQLite boundary, or should we materialize from SQLite into DuckDB periodically?
|
|
54
|
+
|
|
55
|
+
### 2. Write Path
|
|
56
|
+
|
|
57
|
+
- Which tables need to be in SQLite? (attempts, outcomes, events, outputs, sessions)
|
|
58
|
+
- Should blob storage stay on the filesystem (content-addressed .bin files)?
|
|
59
|
+
- How to handle the metadata MAP column — SQLite doesn't have MAP type (use JSON instead?)
|
|
60
|
+
- Schema for SQLite tables — mirror the DuckDB schema with type adaptations?
|
|
61
|
+
|
|
62
|
+
### 3. Read Path
|
|
63
|
+
|
|
64
|
+
Two approaches:
|
|
65
|
+
- **Live ATTACH**: DuckDB ATTACHes SQLite file, views read directly from it. Simple but may have locking issues.
|
|
66
|
+
- **Sync/materialize**: Periodic copy from SQLite → DuckDB tables. More complex but fully decoupled.
|
|
67
|
+
|
|
68
|
+
### 4. Kibitzer Integration
|
|
69
|
+
|
|
70
|
+
- Should kibitzer write to the same `.bird/bird.sqlite` file?
|
|
71
|
+
- What tables does kibitzer need? (tool_calls, mode_changes, patterns, suggestions)
|
|
72
|
+
- Should these be BIRD D-layer tables or kibitzer-specific tables in a `kibitzer` schema?
|
|
73
|
+
- How does the coach's pattern detection query blq's events alongside kibitzer's tool_call history?
|
|
74
|
+
|
|
75
|
+
### 5. Migration
|
|
76
|
+
|
|
77
|
+
- How to migrate existing `.bird/blq.duckdb` data to the new SQLite + DuckDB split?
|
|
78
|
+
- Should `blq.duckdb` still exist (for macros, views, materialized analytics)?
|
|
79
|
+
- Backward compatibility with v1.0 databases?
|
|
80
|
+
|
|
81
|
+
### 6. Performance
|
|
82
|
+
|
|
83
|
+
- SQLite WAL mode write performance for kibitzer's volume (~100 writes/min during active agent session)
|
|
84
|
+
- DuckDB ATTACH read performance vs native table performance
|
|
85
|
+
- Memory impact of keeping both connections open
|
|
86
|
+
|
|
87
|
+
## Research Steps
|
|
88
|
+
|
|
89
|
+
1. Read BIRD v5 spec in full — understand S-layer abstraction
|
|
90
|
+
2. Prototype: create a SQLite DB with BIRD tables, ATTACH from DuckDB, run blq macros
|
|
91
|
+
3. Benchmark: write throughput (SQLite WAL vs DuckDB retry pattern)
|
|
92
|
+
4. Benchmark: read performance (ATTACH vs native tables)
|
|
93
|
+
5. Design the schema mapping (DuckDB types → SQLite types, especially MAP → JSON)
|
|
94
|
+
6. Prototype kibitzer writing tool_calls to the same SQLite DB
|
|
95
|
+
7. Test cross-database queries (kibitzer tool_calls JOIN blq events)
|
|
96
|
+
|
|
97
|
+
## References
|
|
98
|
+
|
|
99
|
+
- BIRD v5 spec: `~/Projects/magic/docs/spec/v5/bird-v5.md`
|
|
100
|
+
- BIRD v4 spec: `~/Projects/magic/docs/bird_spec.md`
|
|
101
|
+
- blq schema: `src/blq/bird_schema.sql`
|
|
102
|
+
- blq BirdStore: `src/blq/bird.py` (Window 1/2 pattern)
|
|
103
|
+
- kibitzer state: `~/Projects/kibitzer/src/kibitzer/state.py`
|
|
104
|
+
- kibitzer architecture: `~/Projects/kibitzer/docs/architecture.md`
|
|
105
|
+
- DuckDB SQLite extension: https://duckdb.org/docs/extensions/sqlite
|
|
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "blq-cli"
|
|
7
|
-
version = "1.0.
|
|
8
|
-
description = "
|
|
7
|
+
version = "1.0.2"
|
|
8
|
+
description = "Build Log Query - capture and query build/test logs with DuckDB"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
11
11
|
requires-python = ">=3.10"
|
|
@@ -58,7 +58,7 @@ mcp = [
|
|
|
58
58
|
blq = "blq.cli:main"
|
|
59
59
|
|
|
60
60
|
[project.urls]
|
|
61
|
-
Homepage = "https://blq-cli.readthedocs.
|
|
61
|
+
Homepage = "https://blq-cli.readthedocs.io/"
|
|
62
62
|
Repository = "https://github.com/teaguesterling/blq-cli"
|
|
63
63
|
Issues = "https://github.com/teaguesterling/blq-cli/issues"
|
|
64
64
|
|
|
@@ -420,13 +420,26 @@ class BirdStore:
|
|
|
420
420
|
).fetchone()
|
|
421
421
|
if result:
|
|
422
422
|
current_version = result[0]
|
|
423
|
-
# Apply any pending migrations
|
|
424
|
-
cls._apply_migrations(conn, current_version)
|
|
425
|
-
return
|
|
426
423
|
except duckdb.Error:
|
|
427
424
|
pass # Table doesn't exist, need to create
|
|
428
425
|
|
|
429
|
-
|
|
426
|
+
if current_version is not None and not force:
|
|
427
|
+
# Existing DB. Only touch it when there's pending migration work or a
|
|
428
|
+
# column a past (possibly silently-failed) migration never added;
|
|
429
|
+
# otherwise this is a fast no-op.
|
|
430
|
+
if not cls._needs_repair(conn, current_version):
|
|
431
|
+
return
|
|
432
|
+
# DuckDB blocks column ALTER/RENAME while views depend on the table
|
|
433
|
+
# (this is what left old DBs stuck without extension_data). Drop all
|
|
434
|
+
# views first so migrations + the reconcile can alter the base tables;
|
|
435
|
+
# re-applying the schema below recreates every view.
|
|
436
|
+
cls._drop_all_views(conn)
|
|
437
|
+
cls._apply_migrations(conn, current_version)
|
|
438
|
+
cls._reconcile_schema(conn)
|
|
439
|
+
|
|
440
|
+
# Load schema from SQL file (idempotent: CREATE TABLE IF NOT EXISTS,
|
|
441
|
+
# INSERT OR IGNORE, CREATE OR REPLACE VIEW — safe to re-apply, and it
|
|
442
|
+
# recreates any views dropped above).
|
|
430
443
|
schema_path = Path(__file__).parent / "bird_schema.sql"
|
|
431
444
|
if schema_path.exists():
|
|
432
445
|
schema_sql = schema_path.read_text()
|
|
@@ -504,6 +517,92 @@ class BirdStore:
|
|
|
504
517
|
|
|
505
518
|
return statements
|
|
506
519
|
|
|
520
|
+
@classmethod
|
|
521
|
+
def _needs_repair(cls, conn: duckdb.DuckDBPyConnection, current_version: str) -> bool:
|
|
522
|
+
"""True if the DB needs migration/repair — pending version bump OR a
|
|
523
|
+
required column a past (possibly silently-failed) migration never added.
|
|
524
|
+
|
|
525
|
+
Keeps the common path a fast no-op: a fully current DB returns False, so
|
|
526
|
+
_ensure_schema doesn't drop/recreate views on every connect.
|
|
527
|
+
"""
|
|
528
|
+
try:
|
|
529
|
+
parts = [int(p) for p in current_version.split(".")]
|
|
530
|
+
major, minor = parts[0], parts[1] if len(parts) > 1 else 0
|
|
531
|
+
if (major, minor) < (3, 0):
|
|
532
|
+
return True
|
|
533
|
+
except (ValueError, IndexError):
|
|
534
|
+
return True # unparseable version — reconcile to be safe
|
|
535
|
+
# Self-heal for migrations that failed silently on dependent views and
|
|
536
|
+
# then let the version advance past themselves (leaving no column).
|
|
537
|
+
for table in ("attempts", "invocations"):
|
|
538
|
+
try:
|
|
539
|
+
has = conn.execute(
|
|
540
|
+
"SELECT 1 FROM information_schema.columns "
|
|
541
|
+
f"WHERE table_name = '{table}' AND column_name = 'extension_data'"
|
|
542
|
+
).fetchone()
|
|
543
|
+
if not has:
|
|
544
|
+
return True
|
|
545
|
+
except duckdb.Error:
|
|
546
|
+
return True
|
|
547
|
+
return False
|
|
548
|
+
|
|
549
|
+
@staticmethod
|
|
550
|
+
def _drop_all_views(conn: duckdb.DuckDBPyConnection) -> None:
|
|
551
|
+
"""Drop all views so base-table column ALTER/RENAME isn't blocked by
|
|
552
|
+
dependents. The schema is re-applied afterward (CREATE OR REPLACE VIEW)
|
|
553
|
+
to recreate them."""
|
|
554
|
+
try:
|
|
555
|
+
views = conn.execute(
|
|
556
|
+
"SELECT table_name FROM information_schema.tables "
|
|
557
|
+
"WHERE table_type = 'VIEW'"
|
|
558
|
+
).fetchall()
|
|
559
|
+
except duckdb.Error:
|
|
560
|
+
return
|
|
561
|
+
for (name,) in views:
|
|
562
|
+
try:
|
|
563
|
+
conn.execute(f'DROP VIEW IF EXISTS "{name}" CASCADE')
|
|
564
|
+
except duckdb.Error:
|
|
565
|
+
pass
|
|
566
|
+
|
|
567
|
+
@classmethod
|
|
568
|
+
def _reconcile_schema(cls, conn: duckdb.DuckDBPyConnection) -> None:
|
|
569
|
+
"""Version-independent self-heal: ensure `extension_data` exists on
|
|
570
|
+
attempts and invocations even if a past migration failed silently (blocked
|
|
571
|
+
by dependent views) and the schema version advanced past it. Views must
|
|
572
|
+
already be dropped by the caller so the ALTER/RENAME can proceed.
|
|
573
|
+
|
|
574
|
+
Uses ADD COLUMN + data copy, NOT rename: RENAME COLUMN on these tables is
|
|
575
|
+
blocked by non-view dependencies (FKs/constraints) that persist even after
|
|
576
|
+
dropping views, whereas ADD COLUMN is never blocked. Existing `sandbox`
|
|
577
|
+
data (from the 2.3 migration) is copied into the new column wrapped as
|
|
578
|
+
{"sandbox": ...}; the now-redundant `sandbox` column is left in place
|
|
579
|
+
(harmless, and DROP COLUMN hits the same dependency wall).
|
|
580
|
+
"""
|
|
581
|
+
for table in ("attempts", "invocations"):
|
|
582
|
+
try:
|
|
583
|
+
cols = {
|
|
584
|
+
r[0]
|
|
585
|
+
for r in conn.execute(
|
|
586
|
+
"SELECT column_name FROM information_schema.columns "
|
|
587
|
+
f"WHERE table_name = '{table}'"
|
|
588
|
+
).fetchall()
|
|
589
|
+
}
|
|
590
|
+
if "extension_data" in cols:
|
|
591
|
+
continue
|
|
592
|
+
conn.execute(f"ALTER TABLE {table} ADD COLUMN extension_data JSON")
|
|
593
|
+
if "sandbox" in cols:
|
|
594
|
+
conn.execute(
|
|
595
|
+
f"UPDATE {table} SET extension_data = "
|
|
596
|
+
f"json_object('sandbox', sandbox) WHERE sandbox IS NOT NULL"
|
|
597
|
+
)
|
|
598
|
+
logger.info(
|
|
599
|
+
f"Repaired {table}: added extension_data (copied sandbox data)"
|
|
600
|
+
)
|
|
601
|
+
else:
|
|
602
|
+
logger.info(f"Repaired {table}: added extension_data")
|
|
603
|
+
except duckdb.Error as e:
|
|
604
|
+
logger.warning(f"Schema reconcile warning ({table}): {e}")
|
|
605
|
+
|
|
507
606
|
@classmethod
|
|
508
607
|
def _apply_migrations(cls, conn: duckdb.DuckDBPyConnection, current_version: str) -> None:
|
|
509
608
|
"""Apply schema migrations from current version to latest.
|
|
@@ -557,42 +656,15 @@ class BirdStore:
|
|
|
557
656
|
# Update schema version
|
|
558
657
|
conn.execute("UPDATE blq_metadata SET value = '2.3.0' WHERE key = 'schema_version'")
|
|
559
658
|
|
|
560
|
-
# Migration: 2.3.0 -> 2.4.0 (
|
|
659
|
+
# Migration: 2.3.0 -> 2.4.0 (sandbox -> extension_data).
|
|
660
|
+
# RENAME COLUMN is blocked by non-view dependencies on these tables, so
|
|
661
|
+
# this is done as ADD + copy via _reconcile_schema (which is idempotent
|
|
662
|
+
# and always succeeds), rather than the RENAME that silently failed and
|
|
663
|
+
# left old DBs stuck without extension_data.
|
|
561
664
|
if (major, minor) < (2, 4):
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
# Check if old column exists
|
|
566
|
-
result = conn.execute(
|
|
567
|
-
"SELECT column_name FROM information_schema.columns "
|
|
568
|
-
f"WHERE table_name = '{table}' AND column_name = 'sandbox'"
|
|
569
|
-
).fetchone()
|
|
570
|
-
if result:
|
|
571
|
-
conn.execute(f"ALTER TABLE {table} RENAME COLUMN sandbox TO extension_data")
|
|
572
|
-
# Wrap existing sandbox data in {"sandbox": ...} envelope
|
|
573
|
-
conn.execute(
|
|
574
|
-
f"UPDATE {table} SET extension_data = "
|
|
575
|
-
f"json_object('sandbox', extension_data) "
|
|
576
|
-
f"WHERE extension_data IS NOT NULL"
|
|
577
|
-
)
|
|
578
|
-
logger.info(f"Migration: Renamed sandbox to extension_data in {table}")
|
|
579
|
-
migrations_applied = True
|
|
580
|
-
else:
|
|
581
|
-
# Check if new column exists, add if not
|
|
582
|
-
result = conn.execute(
|
|
583
|
-
"SELECT column_name FROM information_schema.columns "
|
|
584
|
-
f"WHERE table_name = '{table}' AND column_name = 'extension_data'"
|
|
585
|
-
).fetchone()
|
|
586
|
-
if not result:
|
|
587
|
-
conn.execute(f"ALTER TABLE {table} ADD COLUMN extension_data JSON")
|
|
588
|
-
logger.info(f"Migration: Added extension_data column to {table}")
|
|
589
|
-
migrations_applied = True
|
|
590
|
-
except duckdb.Error as e:
|
|
591
|
-
logger.warning(f"Migration warning: {e}")
|
|
592
|
-
migration_ok = False
|
|
593
|
-
|
|
594
|
-
if migration_ok:
|
|
595
|
-
conn.execute("UPDATE blq_metadata SET value = '2.4.0' WHERE key = 'schema_version'")
|
|
665
|
+
cls._reconcile_schema(conn)
|
|
666
|
+
migrations_applied = True
|
|
667
|
+
conn.execute("UPDATE blq_metadata SET value = '2.4.0' WHERE key = 'schema_version'")
|
|
596
668
|
|
|
597
669
|
# Migration: 2.4.0 -> 3.0.0 (directory rename .lq -> .bird, no table changes)
|
|
598
670
|
if (major, minor) < (3, 0):
|
|
@@ -1193,7 +1265,7 @@ class BirdStore:
|
|
|
1193
1265
|
)
|
|
1194
1266
|
|
|
1195
1267
|
# Clean up any stale lock files left by orphaned processes
|
|
1196
|
-
locks_dir = self.
|
|
1268
|
+
locks_dir = self._lq_dir / "locks"
|
|
1197
1269
|
cleanup_stale_locks(locks_dir)
|
|
1198
1270
|
|
|
1199
1271
|
return orphaned_ids
|
|
@@ -1330,9 +1330,7 @@ def main() -> None:
|
|
|
1330
1330
|
cmd_sandbox_tighten,
|
|
1331
1331
|
)
|
|
1332
1332
|
|
|
1333
|
-
p_sandbox = subparsers.add_parser(
|
|
1334
|
-
"sandbox", help="Manage sandbox specifications"
|
|
1335
|
-
)
|
|
1333
|
+
p_sandbox = subparsers.add_parser("sandbox", help="Manage sandbox specifications")
|
|
1336
1334
|
sandbox_subparsers = p_sandbox.add_subparsers(
|
|
1337
1335
|
dest="sandbox_command", help="Sandbox subcommands"
|
|
1338
1336
|
)
|
|
@@ -1358,9 +1356,7 @@ def main() -> None:
|
|
|
1358
1356
|
p_sandbox_suggest.set_defaults(func=cmd_sandbox_suggest)
|
|
1359
1357
|
|
|
1360
1358
|
# sandbox profile
|
|
1361
|
-
p_sandbox_profile = sandbox_subparsers.add_parser(
|
|
1362
|
-
"profile", help="Profile command with strace"
|
|
1363
|
-
)
|
|
1359
|
+
p_sandbox_profile = sandbox_subparsers.add_parser("profile", help="Profile command with strace")
|
|
1364
1360
|
p_sandbox_profile.add_argument("command", help="Command name to profile")
|
|
1365
1361
|
p_sandbox_profile.add_argument(
|
|
1366
1362
|
"--json", "-j", action="store_true", help="Output raw profile as JSON"
|
|
@@ -835,8 +835,7 @@ class BlqConfig:
|
|
|
835
835
|
legacy_path.rename(bird_path)
|
|
836
836
|
logger.info(f"Migrated {legacy_path} → {bird_path}")
|
|
837
837
|
print(
|
|
838
|
-
f" Migrated {LEGACY_DIR}/ → {BIRD_DIR}/ "
|
|
839
|
-
f"(legacy directory renamed)",
|
|
838
|
+
f" Migrated {LEGACY_DIR}/ → {BIRD_DIR}/ (legacy directory renamed)",
|
|
840
839
|
file=sys.stderr,
|
|
841
840
|
)
|
|
842
841
|
return cls.load(bird_path)
|
|
@@ -394,7 +394,10 @@ def _format_context_with_read_lines(
|
|
|
394
394
|
conn.execute("""
|
|
395
395
|
CREATE OR REPLACE MACRO blq_read_lines(content, lines_spec, marks := []) AS TABLE
|
|
396
396
|
WITH mark_ranges AS (
|
|
397
|
-
SELECT unnest(
|
|
397
|
+
SELECT unnest(
|
|
398
|
+
marks::STRUCT(start INT, "end" INT, mark VARCHAR)[],
|
|
399
|
+
recursive := true
|
|
400
|
+
)
|
|
398
401
|
)
|
|
399
402
|
SELECT
|
|
400
403
|
l.line_number,
|
|
@@ -458,18 +458,20 @@ def _execute_with_live_output(
|
|
|
458
458
|
dims.append(f"{key}={sandbox[key]}")
|
|
459
459
|
spec_summary = ", ".join(dims) if dims else "custom"
|
|
460
460
|
|
|
461
|
-
events.append(
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
461
|
+
events.append(
|
|
462
|
+
{
|
|
463
|
+
"severity": "info",
|
|
464
|
+
"message": (
|
|
465
|
+
f"Command failed in sandbox ({spec_summary},"
|
|
466
|
+
f" grade_w={grade_w}, effects_ceiling={ceiling})"
|
|
467
|
+
),
|
|
468
|
+
"code": f"sandbox_exit_{exit_code}",
|
|
469
|
+
"fingerprint": hashlib.blake2b(
|
|
470
|
+
f"sandbox:{grade_w}:{ceiling}:{exit_code}".encode(),
|
|
471
|
+
digest_size=8,
|
|
472
|
+
).hexdigest(),
|
|
473
|
+
}
|
|
474
|
+
)
|
|
473
475
|
|
|
474
476
|
# Detect specific sandbox violations in output
|
|
475
477
|
if cmd_spec.extension_data.get("sandbox") and exit_code != 0 and output:
|
|
@@ -478,17 +480,19 @@ def _execute_with_live_output(
|
|
|
478
480
|
sandbox = cmd_spec.extension_data["sandbox"]
|
|
479
481
|
violations = detect_violations(output, sandbox)
|
|
480
482
|
for v in violations:
|
|
481
|
-
events.append(
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
483
|
+
events.append(
|
|
484
|
+
{
|
|
485
|
+
"severity": "info",
|
|
486
|
+
"message": f"Sandbox {v.dimension} violation: {v.pattern}",
|
|
487
|
+
"code": f"sandbox_violation_{v.dimension}",
|
|
488
|
+
"fingerprint": hashlib.blake2b(
|
|
489
|
+
f"sandbox_violation:{v.dimension}:{v.pattern}".encode(),
|
|
490
|
+
digest_size=8,
|
|
491
|
+
).hexdigest(),
|
|
492
|
+
"log_line_start": v.line_number,
|
|
493
|
+
"log_line_end": v.line_number,
|
|
494
|
+
}
|
|
495
|
+
)
|
|
492
496
|
|
|
493
497
|
# =========================================================================
|
|
494
498
|
# Window 2: Post-execution DB access (minimal lock time)
|
|
@@ -536,9 +540,7 @@ def _execute_with_live_output(
|
|
|
536
540
|
|
|
537
541
|
# Update attempt with enriched extension_data (grades, metrics)
|
|
538
542
|
if cmd_spec.extension_data:
|
|
539
|
-
store.update_attempt_extension_data(
|
|
540
|
-
attempt_id, cmd_spec.extension_data
|
|
541
|
-
)
|
|
543
|
+
store.update_attempt_extension_data(attempt_id, cmd_spec.extension_data)
|
|
542
544
|
|
|
543
545
|
# Write events
|
|
544
546
|
store.write_events(
|
|
@@ -508,8 +508,16 @@ def _add_to_gitignore(cwd: Path) -> bool:
|
|
|
508
508
|
for line in lines:
|
|
509
509
|
stripped = line.strip()
|
|
510
510
|
if stripped in (
|
|
511
|
-
".bird",
|
|
512
|
-
".
|
|
511
|
+
".bird",
|
|
512
|
+
".bird/",
|
|
513
|
+
"/.bird",
|
|
514
|
+
"/.bird/",
|
|
515
|
+
".bird/*",
|
|
516
|
+
".lq",
|
|
517
|
+
".lq/",
|
|
518
|
+
"/.lq",
|
|
519
|
+
"/.lq/",
|
|
520
|
+
".lq/*",
|
|
513
521
|
):
|
|
514
522
|
# Already has some form of ignore entry
|
|
515
523
|
# Check if it's the new pattern with exceptions
|
|
@@ -184,8 +184,69 @@ def cmd_mcp_install(args: argparse.Namespace) -> None:
|
|
|
184
184
|
print(" Or set hooks.auto_claude_code = true in user config")
|
|
185
185
|
|
|
186
186
|
|
|
187
|
+
def _apply_mcp_thread_limits() -> None:
|
|
188
|
+
"""Cap DuckDB and asyncio thread pools for the MCP server process.
|
|
189
|
+
|
|
190
|
+
The MCP server is long-lived and mostly idle, so it has no business
|
|
191
|
+
holding DuckDB's default per-CPU threadpool (~36 threads on a 36-core
|
|
192
|
+
host) or asyncio's default ThreadPoolExecutor (`min(32, cpu_count + 4)`).
|
|
193
|
+
Without these caps each `blq mcp serve` process holds ~73 idle threads;
|
|
194
|
+
with `N` Claude sessions that compounds to `N * 73`.
|
|
195
|
+
|
|
196
|
+
Both caps are configurable via env vars (sized for "responsive enough
|
|
197
|
+
for one capture/query at a time" rather than parallel throughput):
|
|
198
|
+
BLQ_MCP_DUCKDB_THREADS — default 2
|
|
199
|
+
BLQ_MCP_ASYNC_WORKERS — default 4
|
|
200
|
+
"""
|
|
201
|
+
import asyncio
|
|
202
|
+
import os
|
|
203
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
204
|
+
|
|
205
|
+
duckdb_threads = max(1, int(os.environ.get("BLQ_MCP_DUCKDB_THREADS", "2")))
|
|
206
|
+
async_workers = max(1, int(os.environ.get("BLQ_MCP_ASYNC_WORKERS", "4")))
|
|
207
|
+
|
|
208
|
+
# 1) DuckDB: wrap duckdb.connect so every connection lowers its thread
|
|
209
|
+
# count immediately after open. There are 24+ duckdb.connect callsites
|
|
210
|
+
# across blq; patching the entry point covers them all without
|
|
211
|
+
# threading config through every caller.
|
|
212
|
+
import duckdb
|
|
213
|
+
|
|
214
|
+
_original_connect = duckdb.connect
|
|
215
|
+
|
|
216
|
+
def _capped_connect(*args, **kwargs): # type: ignore[no-untyped-def]
|
|
217
|
+
conn = _original_connect(*args, **kwargs)
|
|
218
|
+
try:
|
|
219
|
+
conn.execute(f"PRAGMA threads={duckdb_threads}")
|
|
220
|
+
except Exception:
|
|
221
|
+
# Read-only connections or some attachment modes may refuse PRAGMA;
|
|
222
|
+
# leave those at the default rather than crash startup.
|
|
223
|
+
pass
|
|
224
|
+
return conn
|
|
225
|
+
|
|
226
|
+
duckdb.connect = _capped_connect # type: ignore[assignment]
|
|
227
|
+
|
|
228
|
+
# 2) asyncio: install a bounded default executor BEFORE fastmcp starts
|
|
229
|
+
# its event loop. fastmcp uses the running loop's default executor
|
|
230
|
+
# for blocking calls (subprocess waits, file I/O). 4 workers is
|
|
231
|
+
# plenty for the MCP server's "one tool call at a time" workload.
|
|
232
|
+
try:
|
|
233
|
+
loop = asyncio.new_event_loop()
|
|
234
|
+
loop.set_default_executor(
|
|
235
|
+
ThreadPoolExecutor(max_workers=async_workers, thread_name_prefix="blq-async"),
|
|
236
|
+
)
|
|
237
|
+
asyncio.set_event_loop(loop)
|
|
238
|
+
except Exception:
|
|
239
|
+
# If asyncio loop policy disallows replacing the loop here, fastmcp
|
|
240
|
+
# will create its own and we keep the DuckDB cap which is the bigger
|
|
241
|
+
# win anyway.
|
|
242
|
+
pass
|
|
243
|
+
|
|
244
|
+
|
|
187
245
|
def cmd_mcp_serve(args: argparse.Namespace) -> None:
|
|
188
246
|
"""Start the MCP server."""
|
|
247
|
+
# Cap thread pools BEFORE importing serve (which loads fastmcp/duckdb).
|
|
248
|
+
_apply_mcp_thread_limits()
|
|
249
|
+
|
|
189
250
|
from blq.serve import serve
|
|
190
251
|
from blq.user_config import UserConfig
|
|
191
252
|
|
|
@@ -145,11 +145,11 @@ def cmd_sandbox_suggest(args: Any) -> None:
|
|
|
145
145
|
print(f"Error querying metrics: {e}", file=sys.stderr)
|
|
146
146
|
sys.exit(1)
|
|
147
147
|
|
|
148
|
-
|
|
149
|
-
if run_count == 0:
|
|
148
|
+
if not result or result[0] == 0:
|
|
150
149
|
print(f"No runs found for '{cmd_name}'. Run it a few times first.")
|
|
151
150
|
return
|
|
152
151
|
|
|
152
|
+
run_count = result[0]
|
|
153
153
|
max_memory = result[1]
|
|
154
154
|
max_cpu_usec = result[2]
|
|
155
155
|
max_duration_ms = result[3]
|
|
@@ -327,8 +327,8 @@ def cmd_sandbox_tighten(args: Any) -> None:
|
|
|
327
327
|
print(f"Error querying metrics: {e}", file=sys.stderr)
|
|
328
328
|
sys.exit(1)
|
|
329
329
|
|
|
330
|
-
|
|
331
|
-
|
|
330
|
+
if not result or result[0] < 3:
|
|
331
|
+
run_count = result[0] if result else 0
|
|
332
332
|
print(
|
|
333
333
|
f"Insufficient data: only {run_count} run(s) found for '{cmd_name}'. "
|
|
334
334
|
"At least 3 runs are required for reliable tightening.",
|
|
@@ -357,7 +357,7 @@ def cmd_sandbox_tighten(args: Any) -> None:
|
|
|
357
357
|
return
|
|
358
358
|
|
|
359
359
|
# Show diff
|
|
360
|
-
from blq_sandbox.spec import
|
|
360
|
+
from blq_sandbox.spec import format_duration, format_size
|
|
361
361
|
|
|
362
362
|
print(f"Tightening sandbox spec for '{cmd_name}' (based on {run_count} runs):")
|
|
363
363
|
print()
|
|
@@ -69,7 +69,10 @@ def cmd_sync(args: argparse.Namespace) -> None:
|
|
|
69
69
|
# Validate project info exists
|
|
70
70
|
if not config.namespace or not config.project:
|
|
71
71
|
print("Error: Project namespace/project not configured.", file=sys.stderr)
|
|
72
|
-
print(
|
|
72
|
+
print(
|
|
73
|
+
"Run 'blq init --namespace X --project Y' or set in .bird/config.toml",
|
|
74
|
+
file=sys.stderr,
|
|
75
|
+
)
|
|
73
76
|
sys.exit(1)
|
|
74
77
|
|
|
75
78
|
# Get sync parameters
|
|
@@ -5,6 +5,7 @@ Provides:
|
|
|
5
5
|
- RunContext: lazy, DB-backed access to a stored run
|
|
6
6
|
- Annotator protocol + dispatch: plugin discovery and execution
|
|
7
7
|
"""
|
|
8
|
+
|
|
8
9
|
from __future__ import annotations
|
|
9
10
|
|
|
10
11
|
import json
|
|
@@ -32,9 +33,7 @@ class Annotation:
|
|
|
32
33
|
|
|
33
34
|
def __post_init__(self) -> None:
|
|
34
35
|
if self.display not in VALID_DISPLAYS:
|
|
35
|
-
raise ValueError(
|
|
36
|
-
f"display must be one of {VALID_DISPLAYS}, got {self.display!r}"
|
|
37
|
-
)
|
|
36
|
+
raise ValueError(f"display must be one of {VALID_DISPLAYS}, got {self.display!r}")
|
|
38
37
|
|
|
39
38
|
def to_dict(self) -> dict[str, Any]:
|
|
40
39
|
return {
|
|
@@ -92,10 +91,7 @@ class RunContext:
|
|
|
92
91
|
[self._invocation_id],
|
|
93
92
|
).fetchall()
|
|
94
93
|
columns = [
|
|
95
|
-
desc[0]
|
|
96
|
-
for desc in self._conn.execute(
|
|
97
|
-
"SELECT * FROM events LIMIT 0"
|
|
98
|
-
).description
|
|
94
|
+
desc[0] for desc in self._conn.execute("SELECT * FROM events LIMIT 0").description
|
|
99
95
|
]
|
|
100
96
|
self._events = [dict(zip(columns, row)) for row in rows]
|
|
101
97
|
return self._events
|
|
@@ -124,7 +120,8 @@ class RunContext:
|
|
|
124
120
|
|
|
125
121
|
@property
|
|
126
122
|
def extension_data(self) -> dict[str, Any]:
|
|
127
|
-
|
|
123
|
+
data: dict[str, Any] = self.metadata["extension_data"]
|
|
124
|
+
return data
|
|
128
125
|
|
|
129
126
|
def _load_outcome(self) -> None:
|
|
130
127
|
if not self._outcome_loaded:
|