Experiment/uv tool install#24
Merged
Merged
Conversation
Spike to test whether `uv tool install git+<repo>@<branch>#subdirectory=cli` (a) installs the thin `ultan` entry point from git (light, no torch), and (b) resolves the `retrieval` extra's workspace-sibling (agent-mem-search) from the same git repo. - root pyproject.toml: virtual uv workspace (members: cli, tools/search). - cli/: thin `ultan` package, base deps = pyyaml only; heavy retrieval stack behind a `retrieval` extra pointing at agent-mem-search via workspace source. - `ultan doctor` reports whether the retrieval sibling resolved. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Root pyproject is now BOTH the workspace root AND the installable `ultan` package, so the install command drops the #subdirectory fragment: uv tool install git+https://github.com/nickroci/ultan Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The thin wrapper runs the (unchanged) daemon through uvx rather than in-process: `ultan daemon` = `uvx --from "ultan[retrieval] @ git+…" agent-mem-daemon`. uvx provisions + caches the heavy env; the launcher (ultan/_daemon.py) is ~85 lines, and `ensure_running()` is the lazy-start helper the hooks will call. - daemon is now a workspace member; the `retrieval` extra pulls agent-mem-daemon (transitively agent-mem-search + torch) and resolves from git. - daemon/pyproject: agent-mem-search path source -> workspace source. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- ultan/_priming.py: ported the stdlib-only priming client (socket + crude lexical fallback) verbatim from src/hooks — no torch, not even PyYAML. - ultan/_hooks.py: thin `ultan hook <event>` dispatch. user-prompt-submit lazy-starts the daemon (ensure_running) + injects priming; session-start warms the daemon. No heavy imports on the hot path. - tests/test_hook_import.py: guards the invariant — imports `ultan._hooks` in a clean subprocess and asserts torch/sentence-transformers/etc. are NOT loaded and import time is under the ceiling. Measured ~10ms today. - pyproject: dev group (pytest), testpaths, ruff config. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… daemon `ultan mcp` is the server Claude Code launches for the plugin. It starts light (no torch), async-spawns the memory daemon via ensure_running() so the MCP handshake never blocks on the ~25s model boot, then serves an `ultan_recall` tool that proxies to the daemon over the socket (lexical fallback while warming). - ultan/_mcp.py: build_server() (unit-testable construction) + serve(). - mcp added to base deps (light, no ML); lazy-imported only by `ultan mcp`. - import guard test extended: `mcp` must NOT land on the hook hot path either. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Install-from-inside-CC path, following the claude-mem pattern:
- .claude-plugin/plugin.json: manifest; MCP server via `uvx --from <git> ultan mcp`
(self-provisioning, starts once/session, async-spawns the daemon).
- hooks/hooks.json: per-turn hooks call the INSTALLED binary
`${CLAUDE_PLUGIN_DATA}/bin/ultan hook <event>` (no per-prompt uvx overhead);
SessionStart runs the provisioner.
- scripts/ensure-ultan.sh: SessionStart provisioner — `uv tool install` the thin
CLI into ${CLAUDE_PLUGIN_DATA}/bin on first run, then warm the daemon. Never
blocks the session.
End-user install becomes `/plugin install ultan@<marketplace>`. Still needs a
real `/plugin install` test in Claude Code to confirm the env-var/ordering
mechanics. Slash commands (/ultan, /ultan-advisor) + the README come next.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A single-plugin marketplace pointing at the repo root, so the plugin can be installed via: /plugin marketplace add nickroci/ultan then /plugin install ultan@ultan Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a build-system to src/ that flattens its hooks/ + scripts/ flat modules to TOP-LEVEL (same pattern tools/search uses), and add src as a workspace member. Verified: scope, config, _events, _priming_client, _nudges, the hook handlers all install as top-level modules — so in a synced one-venv install every module resolves WITHOUT sys.path hacks. (Enables the sys.path-removal sweep next.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
src/ is now an installable workspace member whose flat hooks/ and
scripts/ modules install as top-level names, so the runtime
sys.path.insert shims that let the standalone scripts find their
siblings are obsolete. Remove them from every affected source module and
hoist the sibling imports (scope/config/_events/aliases/_nudges/...) into
the normal top-of-file import block, dropping the # noqa: E402 markers
that only existed because those imports sat after the sys.path
manipulation.
- src/hooks/_events.py: drop scripts/ sys.path block + unused `import sys`;
keep the deliberately lazy `from config import get_config` (now only
noqa: PLC0415, the E402 no longer applies).
- src/hooks/{post_tool_use,pre_tool_use,session_start,stop,
user_prompt_submit}.py: remove sys.path blocks, hoist imports.
- src/hooks/{pre_compact,session_end}.py: remove sys.path blocks; the
_CODE_ROOT value they passed to snapshot_and_spawn_flush (a real
flush-script anchor, not a sys.path artifact) now comes from the
canonical config.CODE_ROOT instead of a recomputed Path.
- src/scripts/{compile,lint,query}.py: remove _SCRIPTS_DIR sys.path
blocks, hoist claude_agent_sdk/config/utils imports.
- src/scripts/flush.py: drop the sys.path block; keep _SCRIPTS_DIR (still
needed for the COMPILE_SCRIPT constant), moved below the hoisted imports.
- tools/ultan/advisor.py: replace the daemon-dir sys.path.insert + ignored
import with a direct `from agent_mem_daemon import library_tools` (kept
lazy alongside the SDK import because it pulls the heavy embeddings
stack); refresh the stale "runtime sys.path shim" comment.
- uv.lock: workspace lock from `uv lock` so `uv sync --all-packages`
installs every member (incl. src editable) into one venv.
Hook hot path stays torch-free; tests/test_hook_import.py guard passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Captures the non-interactive half of the Claude Code plugin install path
in scripts/validate-plugin.sh: manifest JSON validity, the SessionStart
provisioner installing the thin ultan CLI into ${CLAUDE_PLUGIN_DATA}/bin
(asserting no torch leaks in), the MCP server's JSON-RPC initialize +
tools/list handshake over stdio, and the UserPromptSubmit hook emitting
additionalContext JSON (graceful no-op when no library exists).
Runs entirely in throwaway temp dirs (AGENT_MEM_HOME / CLAUDE_PLUGIN_DATA)
and tears them down on exit, so it never touches the live install.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make tools/ultan a workspace member (agent-mem-tools) with a flat-module hatch install (advisor.py, remember.py, install.py at top level, mirroring tools/search). Add `ultan advisor` and `ultan remember` subcommands that lazy-import the advisor/remember flat modules — kept off the hook hot path, so the import guard (tests/test_hook_import.py) still passes (torch/mcp do not load on the hook path). remember.py / advisor.py each gain a behaviour-preserving run() entrypoint that main() now delegates to; the subcommands call run() directly. ultan now depends on agent-mem, agent-mem-daemon and agent-mem-tools so a plain `uv tool install ultan` pulls everything advisor/remember need. Add commands/ultan.md and commands/ultan-advisor.md (Claude Code slash commands invoking `ultan remember` / `ultan advisor`) and reference them via `"commands": "./commands/"` in .claude-plugin/plugin.json. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`src` (agent-mem) installs editable into the workspace venv, flattening its `hooks/`/`scripts/` modules to top-level names. The test files no longer need the `_THIS_DIR` + `sys.path.insert/append` wedge to import siblings (`_blockers`, `config`, `conftest`, etc.). For each test/conftest: removed the `_THIS_DIR` definition and the `if ... not in sys.path` block (keeping `_THIS_DIR` where it still backs a `HOOK_SCRIPT` path or `hook_runner`), dropped now-unused `import sys`, lifted post-block imports into the normal import block, and removed their `# noqa: E402`. scripts/conftest.py docstring/comments updated to reflect the editable-install resolution. Suites stay green: src 259, daemon 645, tools/search 174, root hook guard 1. ruff clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- advisor.py: library_tools._SERVER_NAME -> .SERVER_NAME (pre-existing bug that would crash /ultan-advisor at runtime; surfaced by the clean import). - marketplace.json: drop undocumented owner.url + the backward-compat metadata block (agent-validated against current CC plugin docs). - uv.lock: relock after the merged dependency graph (ultan now pulls the full stack so advisor/remember work from one install; hook path stays lazy). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… hooks SessionStart provisioner now detaches the full `uv tool install` (which pulls torch and can exceed the 60s SessionStart timeout on a cold cache) so it never blocks the session; a lock prevents concurrent installs. The 6 per-turn hooks guard on the binary existing ([ -x BIN ] && BIN hook X || true) so they no-op cleanly during the install window, then run normally once it lands. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude Code's plugin hooks-file loader expects { "hooks": { <events> } }, not
the events at the top level — install failed with 'expected record, received
undefined' at path ["hooks"].
…eferencing them caused 'Duplicate hooks file'
…ree)
Packaging/install path:
- ship _device.py in the agent-mem-search wheel (daemon died at import on
every built-wheel install; spawn stderr was DEVNULL so it died silently)
- restore the thin base: ultan deps = mcp only; agent-mem-daemon +
agent-mem-tools move behind a real [retrieval] extra that ensure-ultan.sh
installs (ULTAN_SPEC override for local validation); uvx MCP env stays small
- exclude scripts/__init__.py from the agent-mem wheel (stray top-level
__init__.py landed in site-packages root)
- drop dead member uv.locks; key CI cache on the root lock
Capture path (ultan/_hooks.py, ultan/_events.py):
- byte-aware truncation cascade — two large strings or non-ASCII payloads no
longer silently drop the whole payload (reproduced, fixed, test-pinned)
- SessionStart boundary event + pre-compact SessionEnd seal (legacy parity)
- unknown hook event exits 1, not 2 (2 is the block signal); non-UTF-8 stdin
degrades to {} instead of a traceback; second SessionStart hooks.json entry
captures the event with real stdin; PreToolUse no-op spawn removed
Daemon spawn (ultan/_daemon.py, daemon __main__.py):
- spawn output tees to ~/.agent-mem/daemon-spawn.log instead of DEVNULL
(stale-PID refusal was invisible); flock'd backoff stamp; socket probe
context-managed; acquire_pid_file now precedes configure_logging so a
rejected duplicate can't rotate the live daemon's log
Priming fallback (both verbatim copies, sync-pinned by test):
- shared wall-clock deadline across socket + fallback, per-file 256KiB cap,
k<=0 guard (no more empty envelope), docstrings match the code
Scripts:
- ensure-ultan.sh: atomic noclobber lock with ownership token, shim health
probe, never-block guards, installs ultan[retrieval], records installer PID
- validate-plugin.sh: waits for the detached install, validates the LOCAL
tree, structural manifest checks (would catch both prior regressions),
argv-correct pkill, no ls|grep -q SIGPIPE hazard, check 3 matches design
- bin/ultan wrapper: /ultan and /ultan-advisor work for plugin-only users
(plugin bin/ joins the Bash tool PATH)
Plugin completeness:
- ship the ultan-search skill (README promised it; priming text recommends it)
- README: reverse-migration section for /ultan-install users, @main not
experiment branch, compile references gone (also src/README, AGENTS.md,
SCHEMA.md, lint.py orphan/stale checks removed with compile.py)
CI: root + tools/ultan jobs green (smoke test so pytest collects, shared-venv
pyright path, basic typing gate with TODO to graduate, ruff format), root
pytest in pre-commit; src guard test pins the legacy hot path import-thin.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…anch validation Temporary: plugin.json mcpServers + ensure-ultan.sh SPEC (+ README note) track the branch so the plugin install path can be validated end-to-end before merge. Revert this commit when merging to main. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…~60s Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`ultan doctor` now answers "is it working, and if not, where is it stuck": runtime deps, daemon pid + socket (HEALTHY / WARMING / IDLE / BROKEN), priming round-trip latency + lane, capture-stream freshness, library size. Read-only and stdlib-only. The plugin bin/ultan wrapper covers the pre-install half — when the real binary doesn't exist yet it reports whether the background install is running (with the last install.log line), stalled, failed, or not started, instead of a bare error. Quick start points at `ultan doctor`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1. Scholar batches REQUEUE once on agent timeout instead of dropping. scholar.review returns the packets on a first timeout; the worker re-enqueues them with a scholar_retries marker and review drops on a second timeout — one retry, never a loop. (Observed: a lid-close froze the in-flight run and its 2 proposals were permanently lost.) 2. run_typed closes the SDK stream on cancellation. A wait_for timeout abandoned the query generator mid-iteration, leaking the spawned CLI subprocess (observed: 7h40m at ~3% CPU). The CancelledError handler now aclose()s the stream — shielded and time-bounded — which drives the SDK transport's SIGTERM→SIGKILL teardown. 3. User-asserted /ultan memories can no longer be silently vetoed. New boundary validator (validate_user_asserted_filed): a scan whose transcript carries [USER-ASSERTED] turns must file at least one proposal; ModelRetry sends the model back to self-correct, and a persistent refusal fails loudly instead of emitting an empty packet. (Observed: a remember-event scan emitted 0 proposals against the prompt's own file-don't-veto contract.) 4. Decay sweep classifies fence-less catalog files (folder READMEs) as skipped, not errored — errored=142 was exactly the README count, burying real parse failures in constant noise. Real YAML failures now also get a per-file warning instead of a silent counter bump. 654 daemon tests pass (9 new) at 90.5% coverage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…l for branch validation" This reverts commit a72e75d.
The root CI job syncs only the thin base (mcp) — advisor/remember live
behind the [retrieval] extra by design, so their lazy call sites in
ultan/__main__.py typed as unknown under strict pyright. Inline ignores
flip-flop (unnecessary in the full dev venv, required in CI), so ship
stubs/{advisor,remember}.pyi instead: identical signatures in every env,
zero suppressions. Stubs are pinned to the real tools/ultan signatures.
tools/ultan had no dev dependency-group at all, so CI's `uv sync --locked`
installed no ruff/pyright/pytest ('Failed to spawn: ruff'). Added the same
dev group the other members carry.
test_doctor now stubs the runtime-deps probe: the IDLE/WARMING verdicts it
pins are daemon-state classifications, and whether [retrieval] is installed
depends on which env runs the suite. A third test pins the BROKEN path.
Verified against simulated thin CI envs (UV_PROJECT_ENVIRONMENT +
pyright --venvpath), not just the fully-synced dev venv that masked this.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.