0.1.7-rc.2 moved four things this bundle was built on, and every one of them
failed quietly rather than loudly, so the plugin looked installed and did
nothing:
* `ctx.settings.installSection(...)` is gone. The call threw into a
`try/catch` that only logged a warning, so the settings namespace was never
registered and `configure` had nowhere to persist. An entry's exported
Config IS the form now; the namespace is the Loader entry id the bundle's
patch declares, and `ctx.settings.configure({ auto: false })` is what claims
that entry's own page.
* `settingsScope` and the `settings.plugin.item` slot are gone. The card
registered into both, so it never rendered — the tab draws the intersection
of served namespaces and registered cards, and that intersection was empty.
It now registers into `plugins.bundle.config` keyed by the bundle's package
name, over the `configForms` service, and uses the framework's own form
chrome (`SettingsFormModel` / `SettingsForm` / `SettingsValueField` /
`SettingsSecretField`) instead of the hand-rolled equivalent. A save is one
revision-fenced write.
* `ctx.shell.run(spec)` is gone. Every action that runs a command threw
`shell.run is not a function`; the seam is now
`await ctx.shell.execute(ctx.shell.resolve(request)).result()`.
* A job's `owner` is a session id, not the agent object. The wrong owner
registered a job no session could collect or cancel, and the producer's
`done` resolved `output` where the registry reads `result`, so a finished
job handed back nothing.
Two conformance gaps went with them: a Config field that is not marked
`volatile()` is invisible to every settings UI — the form projection keeps only
volatile fields, and an entry with no form is dropped from `settings.describe`
entirely — and `embeddingApiKey` was a plain field, so the key rode every
settings read back to the browser. It is `role('secret')` now: the browser is
told whether a key is stored and never receives the key.
Also declares the `@deepseek-ai/dsh-*` peerDependencies the compatibility gate
reads, so an incompatible runtime refuses this plugin at startup instead of
half-loading it.
Verified against a real 0.1.7-rc.2 boot: `settings.describe` serves the entry
with `autoGenerate: false`, the key redacted and reported only as `set: true`,
and no startup diagnostics. The host mock in the tests now has no `run` and no
`installSection`, so those seams are contract-tested rather than assumed, and
the browser tests read the installed harness's own slot catalog and package
exports — a rename fails a test instead of quietly emptying a page.
A review on a real repository indexed 113,768 code units and began an embed
that would have run for about eight hours. Two independent causes, both mine:
1. Nothing excluded anything. Slopo does NOT read .gitignore, so a repo with
node_modules in its ignore file still had all 21,104 files under it parsed -
13,091 of them source. The generated config carried no source_dir_exclude at
all, so the tool reviewed the dependencies instead of the code.
2. Nothing bounded the work. Slopo's own >1000 guard is useless here: it
measures the PRE-EXISTING backlog before indexing, and a fresh database has
no units at all. The guard that exists to protect an agent's latency is
silent in the one case that most needs it.
Measured on that repository:
no excludes 113,768 units ~7.9 h
default excludes 8,607 units ~36 min
default + a server-only scope 1,519 units ~6 min
So: default excludes for vendored and generated directories, ADDED to the
user's own list rather than chosen between (a settings patch replaces a whole
value, so a single sourceDirExclude pattern would otherwise re-enable the 13x
blow-up). And every action now indexes first, checks the backlog against
maxIndexUnits (default 5,000), and refuses with the count, the estimate and the
lever rather than starting the work.
The count comes from slopo's own database via node:sqlite - no new dependency,
and it degrades to the summary line on an older runtime or a locked file. That
matters because `Indexed N` reports only what was NEWLY indexed: on a warm
database it reads zero while the backlog is still full, which is exactly the
state a refusal leaves behind. Caught that hole by running the thing against the
real repository rather than trusting the first version.
Also fixes `status` reporting when the endpoint probe succeeded - the fix from
the previous commit is what made this run reachable at all.
The probe returned `error: undefined` on a dimension match. `undefined` is not a
JSON value -- JSON.stringify silently drops it from an object -- so the shape
looked harmless and was not: the harness enforces the declared output schema
against the canonical value before materialising it, and an explicit undefined
property fails that check. A perfectly healthy status call could not be returned
at all.
Fixed at the boundary rather than at each site. Every return now funnels through
`toJsonValue`, which drops undefined properties exactly as JSON.stringify would,
maps non-finite numbers to null (same reasoning: JSON.stringify(NaN) is "null"),
turns an undefined array slot into null rather than shifting indices, and refuses
a circular value instead of recursing forever. `update` was carrying
`errorCode: undefined` and `ensureSlopo` a `detail: undefined` on the same path,
so a per-site fix would have left two more of these waiting.
The probe also now omits `error` rather than assigning undefined, since it is a
plausible thing for another consumer to read.
Four regression tests, and they were checked against a negative control: with the
sanitiser reverted and the undefined restored, exactly the "probe SUCCEEDS" test
fails with "does not survive a JSON round trip" and the others still pass, which
is what makes it a test of this bug rather than of the JSON round trip in
general.
Slopo reports a bad embedding configuration as the two words "Embedding failed."
on stdout, with the real reason in its agent log. That is a slow way to learn
that a model string has the wrong provider prefix, or that the dimensions do not
match what the model actually returns — the second of which is expensive, because
slopo freezes the dimensions into the index at first use.
`status` now does three things instead of one: it reports the resolved settings,
it runs `slopo --version` through the shell seam, and it makes one tiny embedding
request against the configured endpoint and compares the returned width with the
configured one. Verified against a real Ollama: a correct config reports ok, a
dimension mismatch names both numbers, and a wrong model returns the endpoint's
own 404 rather than a silent failure minutes into an embed.
The probe is a direct fetch rather than a shell call, because it is one small
HTTP request and does not belong behind a boundary that exists to confine
filesystem writes.
Two smaller things this needed:
- the sandbox policy is now resolved once at the top of execute, so `status` can
use it without a second resolution
- provider-prefix stripping is an allow-list, not "drop the first segment".
`unclemusclez/jina-embeddings-v2-base-code:latest` is a real model name with no
prefix at all, and an unknown first segment is left alone so a wrong guess is
visible in the reported `sentModel` rather than silent
Three things live here: where the plugin keeps its files, how it renders a
slopo config, and how it finds, installs and invokes the binary.
The adapter is driven by behaviour observed against slopo 0.8.0 rather than by
its documentation:
* --config is a global option and must precede the subcommand
* every path is written absolute, because slopo resolves relative paths
against the process working directory, not against the config file
* unknown config keys are a hard error, so the renderer writes from a closed
allow-list
* the embedding API key is never written at all -- slopo reads
SLOPO_EMBEDDING_API_KEY from the environment ahead of the file, so the
secret stays out of every file this plugin touches. Enforced in pruneConfig
rather than at the call site, so no future caller can leak it by passing one.
explainFailure maps slopo's deliberately terse agent-facing messages onto a
message and a next step, and reads the log tail because stderr alone leaves the
agent with no move: on a real failure stderr said "Embedding failed." while the
log named the exact litellm parameter that was rejected.
installEnv redirects UV_TOOL_DIR, UV_TOOL_BIN_DIR and UV_PYTHON_INSTALL_DIR
into $DSH_HOME. Only two directories in this container are persistent mounts;
~/.local, where uv installs by default, is on the container's writable layer
and does not survive a recreation.
Every path the plugin owns is under $DSH_HOME. Only two directories in the
container this runs in are persistent volumes ($DSH_HOME and the workspace
root); $HOME/.local -- where 'uv tool install' puts slopo and its CPython by
default -- is on the container's writable layer and dies on recreation, not
merely on restart.
renderSlopoConfig writes from a closed allow-list because slopo treats an
unknown config key as a hard error, and quotes every scalar because a path
can contain a colon or a leading dash. Verified against slopo 0.8.0 itself:
it reads the generated file back with no complaint and round-trips all 19
keys. The embedding API key is deliberately absent -- slopo reads
SLOPO_EMBEDDING_API_KEY from the environment ahead of the YAML value, so the
secret never lands on disk.