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`.
This commit is contained in:
1 parent
7261d46ee2
commit
a48e05585e
7 files changed
+93
-48
No files matched your search
@@ -62,7 +62,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 +92,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 +391,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
@@ -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
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
+41
-27
@@ -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
|
||||||
embeddingApiBase: API_BASE,
|
// resolved through the plugin's own schema — so this script can never drift
|
||||||
embeddingModel: MODEL,
|
// from the Config the plugin validates against. A hand-written literal used to
|
||||||
embeddingDimensions: 768,
|
// stand here, and it was missing ten fields: harmless while nothing checked,
|
||||||
embeddingApiKey: '',
|
// and a refused call the moment `execute` began validating the section it was
|
||||||
embeddingBatchSize: 32,
|
// handed.
|
||||||
embeddingConcurrency: 4,
|
const config = settingsSnapshot(
|
||||||
embeddingTimeoutMs: 120_000,
|
Config({
|
||||||
extensions: [],
|
embeddingApiBase: API_BASE,
|
||||||
exclude: [],
|
embeddingModel: MODEL,
|
||||||
maxFiles: 20_000,
|
embeddingBatchSize: 32,
|
||||||
maxFileBytes: 1_048_576,
|
embeddingConcurrency: 4,
|
||||||
chunkLines: 48,
|
embeddingTimeoutMs: 120_000,
|
||||||
maxChunkLines: 80,
|
excerptLines: 40,
|
||||||
overlapLines: 8,
|
syncBudgetMs: BUDGET_MS,
|
||||||
defaultLimit: 8,
|
}),
|
||||||
maxLimit: 25,
|
)
|
||||||
perFileLimit: 2,
|
|
||||||
excerptLines: 40,
|
|
||||||
minSimilarity: 0.2,
|
|
||||||
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}`)
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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', () => {
|
||||||
|
|||||||
Reference in new issue
Block a user