mirror of
https://git.sync.wtf/troed/oc-ls-stats.git
synced 2026-08-31 09:43:38 +03:00
docs: implementation plan for session-scoped server selection
This commit is contained in:
@@ -0,0 +1,319 @@
|
||||
# Session-Scoped Server Selection Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Poll only the llama-server backing the current session's model provider, falling back to polling all detected servers when the mapping is unknown; fold the result into the existing v1.3.0 release.
|
||||
|
||||
**Architecture:** New pure function `selectSessionUrls(candidates, providers, providerID)` in `src/stats.ts` narrows the candidate URL list per poll cycle using the route session's `providerID` matched against `api.state.provider`. `tui.tsx` calls it inside `pollMetrics`. `resolveServerUrls` additionally dedupes detected lists.
|
||||
|
||||
**Tech Stack:** TypeScript, node:test via `npx -y tsx test-backoff.ts` (the repo has no node_modules/tsconfig; `npm run prepack` runs the same test file).
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-08-21-session-scoped-server-selection-design.md`
|
||||
|
||||
---
|
||||
|
||||
### Task 1: `selectSessionUrls` in src/stats.ts
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/stats.ts` (append after `resolveServerUrls`, end of file)
|
||||
- Test: `test-backoff.ts`
|
||||
|
||||
- [ ] **Step 1: Write failing tests**
|
||||
|
||||
Add to the end of `test-backoff.ts`:
|
||||
|
||||
```ts
|
||||
test("selectSessionUrls narrows to the matching provider's baseURL", () => {
|
||||
const candidates = ["http://a:1", "http://b:2"]
|
||||
const providers = [
|
||||
{ id: "vision", name: "Vision", options: { baseURL: "http://c:3" } },
|
||||
{ id: "llamacpp-a", name: "Llama A", options: { baseURL: "http://b:2/v1" } },
|
||||
]
|
||||
assert.deepEqual(selectSessionUrls(candidates, providers, "llamacpp-a"), ["http://b:2"])
|
||||
})
|
||||
|
||||
test("selectSessionUrls matches base_url variant", () => {
|
||||
const candidates = ["http://localhost:9090"]
|
||||
const providers = [{ id: "llamacpp-b", name: "L", options: { base_url: "http://localhost:9090" } }]
|
||||
assert.deepEqual(selectSessionUrls(candidates, providers, "llamacpp-b"), ["http://localhost:9090"])
|
||||
})
|
||||
|
||||
test("selectSessionUrls normalizes /v1 before comparing", () => {
|
||||
const candidates = ["http://x:9"]
|
||||
const providers = [{ id: "p", name: "P", options: { baseURL: "http://x:9/v1" } }]
|
||||
assert.deepEqual(selectSessionUrls(candidates, providers, "p"), ["http://x:9"])
|
||||
})
|
||||
|
||||
test("selectSessionUrls returns candidates unchanged when provider not found", () => {
|
||||
const candidates = ["http://a:1"]
|
||||
assert.deepEqual(selectSessionUrls(candidates, [], "nope"), ["http://a:1"])
|
||||
})
|
||||
|
||||
test("selectSessionUrls returns candidates unchanged when matched provider URL is not a candidate", () => {
|
||||
const candidates = ["http://a:1"]
|
||||
const providers = [{ id: "ollama", name: "Ollama", options: { baseURL: "http://d:4" } }]
|
||||
assert.deepEqual(selectSessionUrls(candidates, providers, "ollama"), ["http://a:1"])
|
||||
})
|
||||
|
||||
test("selectSessionUrls returns candidates unchanged for missing or empty providerID", () => {
|
||||
assert.deepEqual(selectSessionUrls(["http://a:1"], [], undefined), ["http://a:1"])
|
||||
assert.deepEqual(selectSessionUrls(["http://a:1"], [], ""), ["http://a:1"])
|
||||
})
|
||||
|
||||
test("selectSessionUrls returns empty candidates unchanged", () => {
|
||||
assert.deepEqual(selectSessionUrls([], [], "p"), [])
|
||||
})
|
||||
|
||||
test("selectSessionUrls tolerates non-array and malformed providers", () => {
|
||||
const c = ["http://a:1"]
|
||||
assert.deepEqual(selectSessionUrls(c, undefined, "p"), c)
|
||||
assert.deepEqual(selectSessionUrls(c, [null, 42, {}, { id: "p" }], "p"), c)
|
||||
})
|
||||
|
||||
test("selectSessionUrls first match wins for duplicate ids", () => {
|
||||
const candidates = ["http://a:1", "http://b:2"]
|
||||
const providers = [
|
||||
{ id: "dup", name: "first", options: { baseURL: "http://b:2/v1" } },
|
||||
{ id: "dup", name: "second", options: { baseURL: "http://a:1" } },
|
||||
]
|
||||
assert.deepEqual(selectSessionUrls(candidates, providers, "dup"), ["http://b:2"])
|
||||
})
|
||||
```
|
||||
|
||||
Also update the import line at the top of `test-backoff.ts` to include `selectSessionUrls` alongside the existing stats imports.
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `npx -y tsx test-backoff.ts`
|
||||
Expected: FAIL — `does not provide an export named 'selectSessionUrls'`.
|
||||
|
||||
- [ ] **Step 3: Implement**
|
||||
|
||||
Append to `src/stats.ts` after `resolveServerUrls`:
|
||||
|
||||
```ts
|
||||
export function selectSessionUrls(
|
||||
candidates: string[],
|
||||
providers: unknown,
|
||||
providerID: string | undefined,
|
||||
): string[] {
|
||||
if (!providerID || providerID.trim() === "" || candidates.length === 0) return candidates
|
||||
if (!Array.isArray(providers)) return candidates
|
||||
for (const entry of providers) {
|
||||
if (!entry || typeof entry !== "object") continue
|
||||
const provider = entry as Record<string, unknown>
|
||||
if (provider.id !== providerID) continue
|
||||
const opts = provider.options
|
||||
if (opts && typeof opts === "object") {
|
||||
const baseURL =
|
||||
(opts as Record<string, unknown>).baseURL || (opts as Record<string, unknown>).base_url
|
||||
if (typeof baseURL === "string") {
|
||||
const normalized = stripBaseUrlPath(baseURL)
|
||||
if (candidates.includes(normalized)) return [normalized]
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
return candidates
|
||||
}
|
||||
```
|
||||
|
||||
Note the `break`: the first entry whose `id` equals `providerID` decides the outcome; later duplicates are never consulted.
|
||||
|
||||
- [ ] **Step 4: Run tests to verify they pass**
|
||||
|
||||
Run: `npx -y tsx test-backoff.ts`
|
||||
Expected: PASS — all prior 21 plus the new ones, 0 failures.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/stats.ts test-backoff.ts
|
||||
git commit -m "feat: narrow polled servers to the current session's provider"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Dedup detected lists in `resolveServerUrls`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/stats.ts` (`resolveServerUrls` body, last four lines of the function)
|
||||
- Test: `test-backoff.ts`
|
||||
|
||||
- [ ] **Step 1: Write failing test**
|
||||
|
||||
Append to `test-backoff.ts`:
|
||||
|
||||
```ts
|
||||
test("duplicate provider URLs are deduped", () => {
|
||||
const providers = [
|
||||
{ id: "llamacpp-a", name: "A", options: { baseURL: "http://same:7" } },
|
||||
{ id: "llamacpp-b", name: "B", options: { baseURL: "http://same:7/v1" } },
|
||||
]
|
||||
assert.deepEqual(resolveServerUrls(undefined, providers, {}), ["http://same:7"])
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run to verify failure**
|
||||
|
||||
Run: `npx -y tsx test-backoff.ts`
|
||||
Expected: FAIL — actual result contains `"http://same:7"` twice.
|
||||
|
||||
- [ ] **Step 3: Implement**
|
||||
|
||||
In `src/stats.ts`, change the tail of `resolveServerUrls` from:
|
||||
|
||||
```ts
|
||||
const detected = extractProviderUrlsFromList(providers)
|
||||
if (detected.length > 0) return detected
|
||||
const legacy = extractProviderUrls(config)
|
||||
return legacy.length > 0 ? legacy : ["http://localhost:8080"]
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```ts
|
||||
const detected = [...new Set(extractProviderUrlsFromList(providers))]
|
||||
if (detected.length > 0) return detected
|
||||
const legacy = [...new Set(extractProviderUrls(config))]
|
||||
return legacy.length > 0 ? legacy : ["http://localhost:8080"]
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run tests to verify pass**
|
||||
|
||||
Run: `npx -y tsx test-backoff.ts`
|
||||
Expected: PASS, 0 failures.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/stats.ts test-backoff.ts
|
||||
git commit -m "fix: dedupe discovered server URLs"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Wire session-scoped selection into tui.tsx
|
||||
|
||||
**Files:**
|
||||
- Modify: `tui.tsx` (import line ~5; `pollMetrics` body lines ~183–197)
|
||||
|
||||
No unit-test step: `tui.tsx` is host wiring, untested by the suite (established convention). Verify via grep + test run.
|
||||
|
||||
- [ ] **Step 1: Extend the import from `./src/stats`**
|
||||
|
||||
The import at the top currently includes `resolveServerUrls`; add `selectSessionUrls` so it reads (keep alphabetical order as in the file):
|
||||
|
||||
```tsx
|
||||
import { backoffDelayMs, fetchSlots, processPoll, resolveServerUrls, selectSessionUrls } from "./src/stats"
|
||||
```
|
||||
|
||||
(Match the exact current import statement style — single import from `./src/stats`.)
|
||||
|
||||
- [ ] **Step 2: Capture providerID and select per cycle**
|
||||
|
||||
In `pollMetrics`, replace:
|
||||
|
||||
```tsx
|
||||
let model = llamaServerModel
|
||||
if (!model) {
|
||||
const routeSessionID = api.route.current.params?.sessionID
|
||||
if (routeSessionID) {
|
||||
const session = api.state.session.get(routeSessionID)
|
||||
if (session?.model?.id) {
|
||||
model = session.model.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const baseUrl of llamaServerUrls) {
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```tsx
|
||||
let model = llamaServerModel
|
||||
let sessionProviderId: string | undefined
|
||||
if (!model) {
|
||||
const routeSessionID = api.route.current.params?.sessionID
|
||||
if (routeSessionID) {
|
||||
const session = api.state.session.get(routeSessionID)
|
||||
if (session?.model?.id) {
|
||||
model = session.model.id
|
||||
}
|
||||
sessionProviderId = session?.model?.providerID
|
||||
}
|
||||
}
|
||||
|
||||
const activeUrls = selectSessionUrls(llamaServerUrls, api.state.provider, sessionProviderId)
|
||||
if (!activeUrls.length) return
|
||||
for (const baseUrl of activeUrls) {
|
||||
```
|
||||
|
||||
Leave the rest of the loop body untouched. The startup block at ~line 308 (`llamaServerUrls = resolveServerUrls(...)`) stays exactly as is.
|
||||
|
||||
- [ ] **Step 3: Verify**
|
||||
|
||||
Run: `npx -y tsx test-backoff.ts` → all pass.
|
||||
Run: `grep -n "selectSessionUrls\|sessionProviderId\|activeUrls" tui.tsx` → import + the three new usages, nothing else changed.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add tui.tsx
|
||||
git commit -m "feat: poll only the current session's llama-server"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: README note
|
||||
|
||||
**Files:**
|
||||
- Modify: `README.md` (Server Discovery section, after the provider-list paragraph)
|
||||
|
||||
- [ ] **Step 1: Add one sentence**
|
||||
|
||||
After the paragraph beginning "When no explicit `server` option is set…" (the provider-list discovery paragraph), add:
|
||||
|
||||
```markdown
|
||||
When several llama-server providers are detected, stats follow the provider used by the current session; until a session has picked a model, every detected server is polled.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add README.md
|
||||
git commit -m "docs: describe session-scoped server selection"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Fold into the v1.3.0 release
|
||||
|
||||
**Files:**
|
||||
- None modified. Tag/publish operations only.
|
||||
|
||||
- [ ] **Step 1: Full verification**
|
||||
|
||||
Run: `npm run prepack`
|
||||
Expected: all tests pass, exit 0. Working tree clean.
|
||||
|
||||
- [ ] **Step 2: Move tag and push**
|
||||
|
||||
```bash
|
||||
git tag -f v1.3.0
|
||||
git push origin main
|
||||
git push --force origin refs/tags/v1.3.0
|
||||
```
|
||||
|
||||
Expected: tag now points at HEAD (which still has `package.json` version `1.3.0`); remote updated.
|
||||
|
||||
- [ ] **Step 3: Republish**
|
||||
|
||||
`npm publish` publishes to the `@troed` scope registry configured in `~/.npmrc` (git.sync.wtf). If the registry rejects re-publishing an existing version number, retry with `npm publish --force`.
|
||||
|
||||
Verify afterwards: `npm view @troed/oc-ls-stats versions --registry $(npm config get "@troed:registry" 2>/dev/null || npm config get registry)` shows 1.3.0, and the tarball for 1.3.0 contains `selectSessionUrls` in `src/stats.ts` (check `npm pack --dry-run` output lists files, then inspect the packed tarball).
|
||||
|
||||
- [ ] **Step 4: Report**
|
||||
|
||||
Report final commit list since `cfd9d93`, the moved tag SHA, and publish result.
|
||||
Reference in New Issue
Block a user