6 Commits
Author SHA1 Message Date
gitea-actions df2fa0f54b Release v0.1.2 [skip release] [skip actions] 2026-09-26 05:53:08 +00:00
NotExpectedYet 1f42d2a821 Merge branch 'master' of https://git.notexpectedyet.com/Not-Expected-Yet/dsh-recall
Release / release (push) Successful in 6s
2026-09-26 06:52:50 +01:00
NotExpectedYet 269360a4ea docs: lead the install instructions with the published git spec
`dsh plugin --profile web add /path/to/dsh-recall` was the only form given,
which is the development one: it is what you type against a checkout, not what
somebody installing this repository types. The published spec now comes first,
exactly as the maintainer writes it, with the `#semver:^0.1.0` variant beside it
because that is what keeps `dsh plugin update` following release tags — the
Releases section explains why, and this points at it.

The directory form stays, reframed as the alternative it is: a `link:`
dependency that tracks a working tree, so an edit needs no reinstall.

The directory form is the one exercised in a profile here; the remote form
cannot be resolved from this machine, which has no credentials for that host.
2026-09-26 06:51:49 +01:00
gitea-actions 44b73b587e Release v0.1.1 [skip release] [skip actions] 2026-09-26 05:49:16 +00:00
NotExpectedYet b85b107f6d Merge branch 'master' of https://git.notexpectedyet.com/Not-Expected-Yet/dsh-recall
Release / release (push) Successful in 7s
2026-09-26 06:49:01 +01:00
NotExpectedYet a48e05585e fix: remove one machine's addresses and paths from the shipped defaults
The plugin was developed against an Ollama on a private LAN address, and that
address had reached places that ship:

- the `config` parameter description the MODEL reads on every request;
- the commented example in `cordis.patch.yml`, the layer every profile applies;
- `scripts/smoke.mjs` and `scripts/measure.mjs` endpoint defaults;
- both measure scripts' default workspace, which was an absolute local path
  and named a private repository;
- README examples and test fixtures.

All of them now use `http://127.0.0.1:11434`, which is Ollama's documented
default and belongs to no particular machine — kept rather than emptied because
`validateSettings` requires an http(s) URL, so an empty default would turn every
call into INVALID_SETTINGS until someone configured it. Test fixtures use
192.0.2.10 instead: TEST-NET-1 is reserved for documentation and cannot point at
a host anybody runs. The `focus` and `config` examples are generic, and the
workspace defaults are the working directory.

Two script bugs surfaced once the defaults stopped pointing at a live box, and
are fixed here. `measure.mjs` built its own configuration literal, which was ten
fields short of the schema — harmless while nothing validated it, and a refused
call the moment `execute` began checking the section it was handed; it now
resolves its configuration through the plugin's own `Config`, so it cannot drift
again. It also crashed on a failed search (`first.index.scanned` of a result
that has no index) instead of reporting the endpoint it could not reach, which
is now the ordinary case. `scope-measure.mjs` divided by a scope that does not
exist in any workspace without focus markers, and now says so.

Also corrects `cordis.patch.yml`'s description of what the row registers: in
0.1.7 the exported `Config` is the settings registration, not a namespace on
`ctx.settings`.
2026-09-26 06:47:25 +01:00
9 changed files with 117 additions and 42 deletions

No files matched your search

+14
View File
@@ -2,6 +2,20 @@
Every release, newest first, generated from the commits in each release. Every release, newest first, generated from the commits in each release.
## v0.1.2 — 2026-09-26
### Documented
- lead the install instructions with the published git spec
## v0.1.1 — 2026-09-26
### Fixed
- remove one machine's addresses and paths from the shipped defaults
## v0.1.0 — 2026-09-26 ## v0.1.0 — 2026-09-26
### Breaking ### Breaking
+25 -3
View File
@@ -49,6 +49,22 @@ about a millisecond; the only per-query network cost is one embedding call.
## Install ## Install
```sh
dsh plugin --profile web add git+https://git.notexpectedyet.com/Not-Expected-Yet/dsh-recall.git
```
That form installs whatever the default branch holds, pinned to a commit in the
profile's lockfile. The semver fragment installs the newest release tag instead,
and is what makes `dsh plugin update` follow releases — see
[Releases](#releases) for the tag and versioning rules:
```sh
dsh plugin --profile web add 'git+https://git.notexpectedyet.com/Not-Expected-Yet/dsh-recall.git#semver:^0.1.0'
```
To run your own copy instead, add the directory — that becomes a `link:`
dependency which tracks the working tree, so an edit needs no reinstall:
```sh ```sh
dsh plugin --profile web add /path/to/dsh-recall dsh plugin --profile web add /path/to/dsh-recall
``` ```
@@ -62,7 +78,7 @@ a fact:
- id: dsh-recall - id: dsh-recall
name: dsh-recall name: dsh-recall
config: config:
embeddingApiBase: http://10.50.1.2:11434 embeddingApiBase: http://127.0.0.1:11434 # replace with your endpoint
``` ```
Or edit every setting from the UI: **Plugins → dsh-recall → Configure**. The Or edit every setting from the UI: **Plugins → dsh-recall → Configure**. The
@@ -92,7 +108,7 @@ document, so the value survives a restart and stays visible in
Every value is also checkable from the tool, without touching a file: Every value is also checkable from the tool, without touching a file:
``` ```
recall action=configure config={"embeddingApiBase":"http://10.50.1.2:11434"} recall action=configure config={"embeddingApiBase":"http://127.0.0.1:11434"}
``` ```
The API key is declared `role('secret')`, so it is stripped from every settings The API key is declared `role('secret')`, so it is stripped from every settings
@@ -391,11 +407,17 @@ would publish.
## Development ## Development
```sh ```sh
npm test # 91 tests, no network required npm test # the whole suite; no network required
node scripts/smoke.mjs [workspace] [query] # live retrieval check node scripts/smoke.mjs [workspace] [query] # live retrieval check
node scripts/measure.mjs [workspace] [query] [budgetMs] node scripts/measure.mjs [workspace] [query] [budgetMs]
``` ```
Both live scripts default to the endpoint the schema defaults to
(`http://127.0.0.1:11434`) and to a model `ollama pull` can fetch, so they need
no configuration on a local Ollama. Point them at another host with
`RECALL_API_BASE` and `RECALL_MODEL`; `[workspace]` defaults to the working
directory.
The `lib/` tests run anywhere. `test/config.test.js` and `test/plugin.test.js` The `lib/` tests run anywhere. `test/config.test.js` and `test/plugin.test.js`
import `index.js`, which imports `@deepseek-ai/dsh-tools` and import `index.js`, which imports `@deepseek-ai/dsh-tools` and
`@deepseek-ai/schemastery` — resolvable once the plugin is installed into a `@deepseek-ai/schemastery` — resolvable once the plugin is installed into a
+4 -2
View File
@@ -1,7 +1,9 @@
# The layer this bundle contributes to a dsh profile that lists it. # The layer this bundle contributes to a dsh profile that lists it.
# #
# One row: the dsh-recall host plugin, which registers exactly one tool # One row: the dsh-recall host plugin, which registers exactly one tool
# (`recall`) on `ctx.tools` and one settings namespace on `ctx.settings`. # (`recall`) on `ctx.tools`. Its settings are the `Config` schema it exports —
# the Harness keys a form per profile entry by this row's id — so there is no
# section to register and nothing to attach at load.
# #
# There is deliberately NO `config:` block. Every value the plugin needs is # There is deliberately NO `config:` block. Every value the plugin needs is
# either discovered at runtime (the workspace, from the calling session) or # either discovered at runtime (the workspace, from the calling session) or
@@ -14,7 +16,7 @@
# - id: dsh-recall # - id: dsh-recall
# name: dsh-recall # name: dsh-recall
# config: # config:
# embeddingApiBase: http://10.50.1.2:11434 # embeddingApiBase: http://127.0.0.1:11434 # replace with your endpoint
- insert: - insert:
- id: dsh-recall - id: dsh-recall
+7 -6
View File
@@ -32,10 +32,11 @@
* warm-up is available as an explicit background action. * warm-up is available as an explicit background action.
* *
* **Why the endpoint is configuration and not a constant.** The Ollama default * **Why the endpoint is configuration and not a constant.** The Ollama default
* of `127.0.0.1:11434` is a convention, not a fact — this deployment runs Ollama * of `127.0.0.1:11434` is a convention, not a fact — a deployment often runs
* on another host. Baking one machine's address into a bundle layer that other * Ollama on another host. Baking one machine's address into a bundle layer that
* machines apply is exactly the mistake the composition docs warn about, so the * other machines apply is exactly the mistake the composition docs warn about,
* endpoint is a schema default the profile can override, at rest or live. * so the endpoint is a schema default the profile can override, at rest or
* live.
*/ */
import { readFile, stat } from 'node:fs/promises' import { readFile, stat } from 'node:fs/promises'
@@ -965,7 +966,7 @@ export function apply(ctx, config) {
type: 'string', type: 'string',
description: description:
'Scope this whole call — index and search — to one workspace-relative directory, for example ' + 'Scope this whole call — index and search — to one workspace-relative directory, for example ' +
'"src/controllers/room/occupancy". Use it when the session is working on one module: the index ' + '"src/services/billing". Use it when the session is working on one module: the index ' +
'then holds only that module, so its near-identical neighbours cannot outrank it and the first ' + 'then holds only that module, so its near-identical neighbours cannot outrank it and the first ' +
'search does not wait on the rest of the repository. Empty uses the configured focus, then the ' + 'search does not wait on the rest of the repository. Empty uses the configured focus, then the ' +
'nearest enclosing directory that carries a CONTROLLER.md, DEBUG_UI.md or AGENTS.md.', 'nearest enclosing directory that carries a CONTROLLER.md, DEBUG_UI.md or AGENTS.md.',
@@ -980,7 +981,7 @@ export function apply(ctx, config) {
type: 'json', type: 'json',
description: description:
'Settings patch for action "configure", using this plugin\'s own camelCase field names, for ' + 'Settings patch for action "configure", using this plugin\'s own camelCase field names, for ' +
'example {"embeddingApiBase":"http://10.50.1.2:11434"}.', 'example {"embeddingApiBase":"http://127.0.0.1:11434"}.',
}, },
}, },
output: { output: {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "dsh-recall", "name": "dsh-recall",
"version": "0.1.0", "version": "0.1.2",
"private": true, "private": true,
"type": "module", "type": "module",
"description": "Local-embedding code recall for DeepSeek Harness — returns ranked excerpts with line ranges instead of whole files, so the context window stops growing with every read.", "description": "Local-embedding code recall for DeepSeek Harness — returns ranked excerpts with line ranges instead of whole files, so the context window stops growing with every read.",
+34 -20
View File
@@ -19,38 +19,37 @@ import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os' import { tmpdir } from 'node:os'
import { join } from 'node:path' import { join } from 'node:path'
import { apply } from '../index.js' import { apply, Config, settingsSnapshot } from '../index.js'
const WORKSPACE = process.argv[2] ?? '/root/workspaces/ha-runtime' // Whatever repository you run this against, and whatever you want to ask it.
const QUERY = process.argv[3] ?? 'how does the occupancy state machine decide a room is occupied' // The defaults are the working directory and a query that names a behaviour
// rather than a symbol — which is the kind of question the plugin exists for.
const WORKSPACE = process.argv[2] ?? process.cwd()
const QUERY = process.argv[3] ?? 'where is the retry policy applied'
const BUDGET_MS = Number(process.argv[4] ?? 90_000) const BUDGET_MS = Number(process.argv[4] ?? 90_000)
const API_BASE = process.env.RECALL_API_BASE ?? 'http://10.50.1.2:11434' // The endpoint to measure against: the Ollama convention unless RECALL_API_BASE
// says otherwise. Nothing here is specific to the machine this was written on.
const API_BASE = process.env.RECALL_API_BASE ?? 'http://127.0.0.1:11434'
const MODEL = process.env.RECALL_MODEL ?? 'unclemusclez/jina-embeddings-v2-base-code:latest' const MODEL = process.env.RECALL_MODEL ?? 'unclemusclez/jina-embeddings-v2-base-code:latest'
const config = { // The measurement's configuration, stated as the few values it cares about and
// resolved through the plugin's own schema — so this script can never drift
// from the Config the plugin validates against. A hand-written literal used to
// stand here, and it was missing ten fields: harmless while nothing checked,
// and a refused call the moment `execute` began validating the section it was
// handed.
const config = settingsSnapshot(
Config({
embeddingApiBase: API_BASE, embeddingApiBase: API_BASE,
embeddingModel: MODEL, embeddingModel: MODEL,
embeddingDimensions: 768,
embeddingApiKey: '',
embeddingBatchSize: 32, embeddingBatchSize: 32,
embeddingConcurrency: 4, embeddingConcurrency: 4,
embeddingTimeoutMs: 120_000, embeddingTimeoutMs: 120_000,
extensions: [],
exclude: [],
maxFiles: 20_000,
maxFileBytes: 1_048_576,
chunkLines: 48,
maxChunkLines: 80,
overlapLines: 8,
defaultLimit: 8,
maxLimit: 25,
perFileLimit: 2,
excerptLines: 40, excerptLines: 40,
minSimilarity: 0.2,
syncBudgetMs: BUDGET_MS, syncBudgetMs: BUDGET_MS,
stateDir: '', }),
} )
/** The smallest context `apply` touches, with no settings provider mounted. */ /** The smallest context `apply` touches, with no settings provider mounted. */
function context() { function context() {
@@ -92,6 +91,21 @@ async function main() {
const coldMs = Date.now() - coldStart const coldMs = Date.now() - coldStart
console.log('--- FIRST SEARCH (bounded cold start) ---') console.log('--- FIRST SEARCH (bounded cold start) ---')
console.log(`elapsed : ${(coldMs / 1000).toFixed(1)}s (budget ${BUDGET_MS / 1000}s)`) console.log(`elapsed : ${(coldMs / 1000).toFixed(1)}s (budget ${BUDGET_MS / 1000}s)`)
// A search that did not run has no index or results to report, and the tool
// already says why. Without this the first `first.index.*` line throws a
// TypeError that buries the actual cause — an endpoint that is not there is
// the ordinary case now that the default is the loopback convention.
if (first.ok !== true) {
console.log(`failed : ${first.errorCode ?? 'UNKNOWN'}`)
console.log(`note : ${first.note ?? '(none)'}`)
if (first.remedy !== undefined) console.log(`remedy : ${first.remedy}`)
console.log('')
console.log(`nothing to measure: point RECALL_API_BASE at a reachable endpoint (currently ${API_BASE}).`)
process.exitCode = 1
return
}
console.log(`scanned files : ${first.index.scanned}`) console.log(`scanned files : ${first.index.scanned}`)
console.log(`indexed files : ${first.index.files}`) console.log(`indexed files : ${first.index.files}`)
console.log(`chunks : ${first.index.chunks}`) console.log(`chunks : ${first.index.chunks}`)
+23 -6
View File
@@ -6,10 +6,9 @@
* cold-start cost they imply are all arithmetic over the chunker, which is * cold-start cost they imply are all arithmetic over the chunker, which is
* deterministic and offline. * deterministic and offline.
* *
* The cold-start projection uses the throughput actually measured against the * The cold-start projection uses a throughput measured against a live endpoint
* real endpoint on this repository: 2,257 chunks embedded in 90s, about 25 * during development: 2,257 chunks embedded in 90s, about 25 chunks/s. It is a
* chunks/s. It is a projection from a real figure, not a guess, and it is * projection from a real figure, not a guess, and it is labelled as one.
* labelled as a projection.
* *
* node scripts/scope-measure.mjs [workspace] * node scripts/scope-measure.mjs [workspace]
*/ */
@@ -20,9 +19,15 @@ import { join } from 'node:path'
import { chunkFile } from '../lib/chunk.js' import { chunkFile } from '../lib/chunk.js'
import { listCodeFiles } from '../lib/walk.js' import { listCodeFiles } from '../lib/walk.js'
const WORKSPACE = process.argv[2] ?? '/root/workspaces/ha-runtime' // The repository to measure; the working directory by default, so the script
// carries no assumption about where anyone keeps their code.
const WORKSPACE = process.argv[2] ?? process.cwd()
/** Chunks per second, measured on this repository against the live endpoint. */ /**
* Chunks per second, measured against a live endpoint during development
* (2,257 chunks in 90s). A measured constant, not a guess — and a projection
* input, which is why the cold-start figures below are labelled as projections.
*/
const MEASURED_CHUNKS_PER_SECOND = 2257 / 90 const MEASURED_CHUNKS_PER_SECOND = 2257 / 90
/** Count files and chunks for one directory, relative to a base for display. */ /** Count files and chunks for one directory, relative to a base for display. */
@@ -98,6 +103,18 @@ async function main() {
if (scopes.length > 10) console.log(` … and ${scopes.length - 10} more`) if (scopes.length > 10) console.log(` … and ${scopes.length - 10} more`)
console.log('') console.log('')
// A workspace with no marker files has no scopes to compare, and that is a
// normal answer rather than a failure — the plugin falls back to the whole
// workspace there. Say so and stop, instead of dividing by the scope that
// does not exist.
if (scopes.length === 0) {
console.log('=== WHAT SCOPING BUYS ===')
console.log(' nothing here: no directory in this workspace carries a focus marker,')
console.log(' so every call indexes the whole workspace. Add a marker file (see')
console.log(' focusMarkers in the README) to a module you work on to create one.')
return
}
const median = scopes[Math.floor(scopes.length / 2)] const median = scopes[Math.floor(scopes.length / 2)]
const totalScoped = scopes.reduce((sum, scope) => sum + scope.chunks, 0) const totalScoped = scopes.reduce((sum, scope) => sum + scope.chunks, 0)
console.log('=== WHAT SCOPING BUYS ===') console.log('=== WHAT SCOPING BUYS ===')
+4 -1
View File
@@ -19,7 +19,10 @@ import { rankChunks } from '../lib/rank.js'
import { RecallIndex } from '../lib/store.js' import { RecallIndex } from '../lib/store.js'
import { listCodeFiles } from '../lib/walk.js' import { listCodeFiles } from '../lib/walk.js'
const API_BASE = process.env.RECALL_API_BASE ?? 'http://10.50.1.2:11434' // The endpoint to check against. The default is the Ollama convention, which
// is what the plugin's own schema defaults to; point RECALL_API_BASE at your
// own host when it is somewhere else.
const API_BASE = process.env.RECALL_API_BASE ?? 'http://127.0.0.1:11434'
const MODEL = process.env.RECALL_MODEL ?? 'unclemusclez/jina-embeddings-v2-base-code:latest' const MODEL = process.env.RECALL_MODEL ?? 'unclemusclez/jina-embeddings-v2-base-code:latest'
const DIMENSIONS = Number(process.env.RECALL_DIMENSIONS ?? 768) const DIMENSIONS = Number(process.env.RECALL_DIMENSIONS ?? 768)
+5 -3
View File
@@ -42,8 +42,10 @@ test('Config fills every field from an empty input, so apply can rely on default
}) })
test('Config keeps an explicit value and fills the rest', () => { test('Config keeps an explicit value and fills the rest', () => {
const resolved = resolve({ embeddingApiBase: 'http://10.50.1.2:11434', embeddingConcurrency: 8 }) // A TEST-NET-1 address (RFC 5737): reserved for documentation, so a fixture
assert.equal(resolved.embeddingApiBase, 'http://10.50.1.2:11434') // can never point at a host anybody actually runs.
const resolved = resolve({ embeddingApiBase: 'http://192.0.2.10:11434', embeddingConcurrency: 8 })
assert.equal(resolved.embeddingApiBase, 'http://192.0.2.10:11434')
assert.equal(resolved.embeddingConcurrency, 8) assert.equal(resolved.embeddingConcurrency, 8)
assert.equal(resolved.embeddingBatchSize, 16) assert.equal(resolved.embeddingBatchSize, 16)
assert.equal(resolved.embeddingTimeoutMs, 30_000) assert.equal(resolved.embeddingTimeoutMs, 30_000)
@@ -135,7 +137,7 @@ test('validateSettings rejects an endpoint that is not a URL', () => {
// so it is the protocol check that actually catches the common mistake. // so it is the protocol check that actually catches the common mistake.
assert.throws(() => validateSettings(settingsWith({ embeddingApiBase: 'localhost:11434' })), /must be http or https/) assert.throws(() => validateSettings(settingsWith({ embeddingApiBase: 'localhost:11434' })), /must be http or https/)
assert.throws(() => validateSettings(settingsWith({ embeddingApiBase: '' })), /not a URL/) assert.throws(() => validateSettings(settingsWith({ embeddingApiBase: '' })), /not a URL/)
assert.doesNotThrow(() => validateSettings(settingsWith({ embeddingApiBase: 'http://10.50.1.2:11434' }))) assert.doesNotThrow(() => validateSettings(settingsWith({ embeddingApiBase: 'http://192.0.2.10:11434' })))
}) })
test('validateSettings rejects a non-http protocol', () => { test('validateSettings rejects a non-http protocol', () => {