diff --git a/.github/workflows/nightly-install.yml b/.github/workflows/nightly-install.yml index 9fedbc08..c897c926 100644 --- a/.github/workflows/nightly-install.yml +++ b/.github/workflows/nightly-install.yml @@ -96,6 +96,16 @@ jobs: "$UV_TOOL_BIN_DIR/studyloop" --version "$UV_TOOL_BIN_DIR/session-export" --help + - name: Verify the studyloop tool env carries the semantic runtime + # `studyloop web` warms the query encoder in-process at boot, inside + # the studyloop tool venv (not agent-session-tools' own). Without + # these the warm fails at import and the header chip reads + # "semantic: failed" -- invisible to the --version/--help checks. + env: + UV_TOOL_DIR: ${{ runner.temp }}/tools + run: | + "$UV_TOOL_DIR/studyloop/bin/python" -c "import huggingface_hub, numpy, onnxruntime, sqlite_vec, tokenizers" + - name: Verify installed agent definitions # One artefact per planted harness, each a path `install agents` # writes for that tool (installers.py _TOOL_LINKS / _configure_*). diff --git a/CHANGELOG.md b/CHANGELOG.md index 15d8e152..9f290884 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -83,6 +83,16 @@ experience may change before `1.0.0`. drained future was already complete, the pump exited without reading that future's `StopAsyncIteration`. The pump now retrieves the result of a done pull future on every exit path. +- `studyloop web` started from an installed tool no longer shows + **semantic: failed (0.0s)**. The installer co-installed `agent-session-tools` + into the `studyloop` tool venv without its extras, so the semantic runtime + (tokenizers, onnxruntime, huggingface-hub, numpy, sqlite-vec) never reached + the venv that serves the web UI: the boot-time encoder warm failed at import + and search silently stayed lexical-only, while `studyloop doctor` in the same + venv called the encoder check "does not apply". The co-install now carries + `[all]`, and the nightly install job and `scripts/smoke-uv-tool-install.sh` + import the runtime in that venv. Existing installs pick it up with + `./scripts/install.sh --tools-only`. ## [0.5.0] - 2026-09-21 diff --git a/docs/setup-guide.md b/docs/setup-guide.md index 7e7f7172..56df818c 100644 --- a/docs/setup-guide.md +++ b/docs/setup-guide.md @@ -136,10 +136,15 @@ such as `session-export`, `session-query`, and `session-sync` may not appear on ```bash uv sync --all-packages uv tool install --editable './packages/studyloop[all]' \ - --with-editable ./packages/agent-session-tools + --with-editable './packages/agent-session-tools[all]' uv tool install --editable './packages/agent-session-tools[all]' ``` +The `[all]` on `--with-editable` matters: `studyloop web` runs the semantic +search layer inside the `studyloop` tool venv, so co-installing +`agent-session-tools` without its extras leaves the header chip reading +`semantic: failed`. + Prefer `./scripts/install.sh` or `studyloop install tools` for normal source checkout installs because they keep the two tool venvs wired together. @@ -943,7 +948,7 @@ command: ```bash uv sync --all-packages uv tool install --editable './packages/studyloop[all]' \ - --with-editable ./packages/agent-session-tools \ + --with-editable './packages/agent-session-tools[all]' \ --force ``` diff --git a/packages/studyloop/src/studyloop/installers.py b/packages/studyloop/src/studyloop/installers.py index 2ee679d6..6e9613d7 100644 --- a/packages/studyloop/src/studyloop/installers.py +++ b/packages/studyloop/src/studyloop/installers.py @@ -1310,7 +1310,14 @@ def install_workspace_tools( cmd.append(f"{pkg_dir}[all]") elif package_name == "studyloop": cmd.append(f"{pkg_dir}[all]") - cmd.extend(["--with-editable", str(repo_root / "packages" / "agent-session-tools")]) + # [all] on the co-installed package too. `studyloop web` imports + # agent_session_tools IN THIS venv: the server's boot-time encoder + # warm and every hybrid search run tokenizers, onnxruntime, + # huggingface_hub, numpy and sqlite_vec here, not in + # agent-session-tools' own tool venv. Co-installed bare, the warm + # failed at import and the header chip read "semantic: failed". + ast_pkg = repo_root / "packages" / "agent-session-tools" + cmd.extend(["--with-editable", f"{ast_pkg}[all]"]) else: cmd.append(str(pkg_dir)) cmd.append("--editable") diff --git a/packages/studyloop/tests/test_ci_workflow_contract.py b/packages/studyloop/tests/test_ci_workflow_contract.py index be7101fc..c56b73ba 100644 --- a/packages/studyloop/tests/test_ci_workflow_contract.py +++ b/packages/studyloop/tests/test_ci_workflow_contract.py @@ -290,3 +290,24 @@ def test_nightly_installer_job_plants_a_harness_before_running_install_sh() -> N assert verify["env"]["HOME"] == "${{ runner.temp }}/home" for planted_marker in planted: assert planted_marker in verify["run"], f"verify step does not look inside {planted_marker}" + + +def test_nightly_installer_job_proves_the_studyloop_tool_env_carries_the_semantic_runtime() -> None: + """`install.sh` gave the studyloop tool venv no semantic runtime, and the + job's only checks were `--version` and `--help`, which pass without it -- + so every install's `studyloop web` failed its encoder warm at import and + nothing in CI could see it. The job must import what the warm imports, in + the venv that serves `studyloop web`, after the script has run. + """ + steps = _nightly_workflow()["jobs"]["installer"]["steps"] + names = [step.get("name") for step in steps] + wanted = "Verify the studyloop tool env carries the semantic runtime" + assert wanted in names, "the installer job must check the studyloop venv's semantic runtime" + assert names.index(wanted) > names.index("Run scripts/install.sh") + + verify = steps[names.index(wanted)] + assert verify["env"]["UV_TOOL_DIR"] == "${{ runner.temp }}/tools" + assert ( + '"$UV_TOOL_DIR/studyloop/bin/python" -c ' + '"import huggingface_hub, numpy, onnxruntime, sqlite_vec, tokenizers"' + ) in verify["run"] diff --git a/packages/studyloop/tests/test_installers_workspace_tools.py b/packages/studyloop/tests/test_installers_workspace_tools.py index 476ebf1c..c97e0707 100644 --- a/packages/studyloop/tests/test_installers_workspace_tools.py +++ b/packages/studyloop/tests/test_installers_workspace_tools.py @@ -59,7 +59,7 @@ def test_install_workspace_tools_installs_expected_tool_commands(tmp_path: Path) _PY_VER, f"{studyloop_pkg}[all]", "--with-editable", - str(agent_pkg), + f"{agent_pkg}[all]", "--editable", "--force", ], @@ -68,6 +68,29 @@ def test_install_workspace_tools_installs_expected_tool_commands(tmp_path: Path) ] +def test_the_studyloop_tool_env_gets_agent_session_tools_with_its_runtime_extras( + tmp_path: Path, +) -> None: + """`studyloop web` imports agent_session_tools in-process, in the STUDYLOOP + tool venv: the server's boot-time encoder warm and every hybrid search run + tokenizers, onnxruntime, huggingface_hub, numpy and sqlite_vec there. The + standalone agent-session-tools tool gets ``[all]``, but that is a different + venv. Co-installed bare, the studyloop venv had none of them, so the warm + failed at import in ~10 ms and the header chip read ``semantic: failed + (0.0s)`` -- while `doctor`, running in the same venv, called the encoder + check "does not apply". + """ + repo_root = _workspace(tmp_path) + + with patch("studyloop.installers._run") as run: + install_workspace_tools(repo_root, sync_workspace=False) + + studyloop_spec = f"{repo_root / 'packages' / 'studyloop'}[all]" + command = next(c.args[0] for c in run.call_args_list if studyloop_spec in c.args[0]) + co_installed = command[command.index("--with-editable") + 1] + assert co_installed == f"{repo_root / 'packages' / 'agent-session-tools'}[all]" + + def test_install_workspace_tools_can_skip_sync_and_force(tmp_path: Path) -> None: repo_root = _workspace(tmp_path) diff --git a/packages/studyloop/tests/test_smoke_installed_cli_contract.py b/packages/studyloop/tests/test_smoke_installed_cli_contract.py index 3e6373cd..da386565 100644 --- a/packages/studyloop/tests/test_smoke_installed_cli_contract.py +++ b/packages/studyloop/tests/test_smoke_installed_cli_contract.py @@ -67,7 +67,14 @@ def test_smoke_uv_tool_install_uses_isolated_tool_home_and_runs_cli_smoke() -> N 'uv tool install --force --editable --python "$PY_REQUEST" ' '"$ROOT_DIR/packages/studyloop[all]"' ) in script_text - assert '--with-editable "$ROOT_DIR/packages/agent-session-tools"' in script_text + assert '--with-editable "$ROOT_DIR/packages/agent-session-tools[all]"' in script_text + # The studyloop venv serves `studyloop web`, whose encoder warm imports + # the semantic runtime in-process. Import level only: the isolated HOME + # has no Hugging Face cache, and the ONNX artefact is `doctor --fix`'s job. + assert ( + '"$(uv tool dir)/studyloop/bin/python" -c ' + '"import huggingface_hub, numpy, onnxruntime, sqlite_vec, tokenizers"' + ) in script_text assert ( 'uv tool install --force --editable --python "$PY_REQUEST" ' '"$ROOT_DIR/packages/agent-session-tools[all]"' diff --git a/scripts/smoke-uv-tool-install.sh b/scripts/smoke-uv-tool-install.sh index a53ae4ee..fa43a413 100755 --- a/scripts/smoke-uv-tool-install.sh +++ b/scripts/smoke-uv-tool-install.sh @@ -28,9 +28,14 @@ TOOL_BIN="$UV_TOOL_BIN_DIR" PY_REQUEST="$(tr -d '[:space:]' < "$ROOT_DIR/.python-version")" uv tool install --force --editable --python "$PY_REQUEST" "$ROOT_DIR/packages/studyloop[all]" \ - --with-editable "$ROOT_DIR/packages/agent-session-tools" + --with-editable "$ROOT_DIR/packages/agent-session-tools[all]" uv tool install --force --editable --python "$PY_REQUEST" "$ROOT_DIR/packages/agent-session-tools[all]" test -x "$TOOL_BIN/studyloop" test -x "$TOOL_BIN/session-export" +# The studyloop venv serves `studyloop web`, whose boot-time encoder warm +# imports the semantic runtime in-process; without it the header chip reads +# "semantic: failed". Import level only: this isolated HOME has no Hugging +# Face cache, and fetching the ONNX artefact is `doctor --fix`'s job. +"$(uv tool dir)/studyloop/bin/python" -c "import huggingface_hub, numpy, onnxruntime, sqlite_vec, tokenizers" STUDYLOOP_EXPECT_BIN_DIR="$TOOL_BIN" PATH="$TOOL_BIN:$PATH" "$ROOT_DIR/scripts/smoke-installed-cli.sh"