merge: feat/memory-closer-thin-routes (memory sample 6,000 to 30,000) · DreamLab-AI/VisionClaw@9492f49 · GitHub
Skip to content

Commit 9492f49

Browse files
claudejjohare
andcommitted
merge: feat/memory-closer-thin-routes (memory sample 6,000 to 30,000)
Co-Authored-By: jjohare <github@thedreamlab.uk>
2 parents c65cc5a + 562dfef commit 9492f49

18 files changed

Lines changed: 205 additions & 34 deletions

‎client/src/features/visualisation/memoryCloud/MemoryExplorerPanel.tsx‎

Lines changed: 14 additions & 1 deletion

‎client/src/features/visualisation/memoryCloud/__tests__/MemoryExplorerPanel.test.tsx‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,20 @@ describe('MemoryExplorerPanel — explore', () => {
194194
it('shows build progress while the local index builds', () => {
195195
useMemoryCloudStore.setState({ status: 'building', buildProgress: 0.42, snapshot });
196196
render(<MemoryExplorerPanel />);
197-
expect(screen.getByText('Building local index 42%')).toBeInTheDocument();
197+
// names what is being built, so a 30 000-point build reads as one
198+
expect(screen.getByText('Building local index of 1,234 points 42%')).toBeInTheDocument();
199+
});
200+
201+
it('names the sample size while the vectors load', () => {
202+
useMemoryCloudStore.setState({ status: 'loading', loadingCount: 30_000, loadingBytes: 30_000 * 384 * 4, snapshot: null });
203+
render(<MemoryExplorerPanel />);
204+
expect(screen.getByText('Loading the 30,000-point sample (46 MB of vectors)…')).toBeInTheDocument();
205+
});
206+
207+
it('before the snapshot header arrives the loading line claims no size', () => {
208+
useMemoryCloudStore.setState({ status: 'loading', loadingCount: null, loadingBytes: null, snapshot: null });
209+
render(<MemoryExplorerPanel />);
210+
expect(screen.getByText('Loading sample…')).toBeInTheDocument();
198211
});
199212

200213
it('shows a quiet power-user note on 401/403, never an alert', () => {

‎client/src/features/visualisation/memoryCloud/__tests__/memoryCloudStore.test.ts‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,24 @@ describe('memoryCloudStore', () => {
9696
expect(env.engines[0].storageKey).toBe('vc-memory-learning:s1');
9797
});
9898

99+
it('records the sample size from the snapshot header before the vectors arrive', async () => {
100+
let release!: () => void;
101+
const gate = new Promise<void>((r) => (release = r));
102+
const fetchVectors = env.deps.fetchVectors;
103+
env.deps.fetchVectors = vi.fn(async (snap, opts) => {
104+
await gate;
105+
return fetchVectors(snap, opts);
106+
});
107+
const store = createMemoryCloudStore(env.deps);
108+
const p = store.getState().loadSnapshot();
109+
await vi.waitFor(() => expect(store.getState().loadingCount).toBe(3));
110+
expect(store.getState().status).toBe('loading');
111+
expect(store.getState().loadingBytes).toBe(3 * DIM * 4);
112+
release();
113+
await p;
114+
expect(store.getState().status).toBe('ready');
115+
});
116+
99117
it('records build progress from the engine', async () => {
100118
const store = createMemoryCloudStore(env.deps);
101119
await store.getState().loadSnapshot();

‎client/src/features/visualisation/memoryCloud/memoryCloudStore.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,10 @@ export interface MemoryCloudState {
157157
trajectory: TrajectoryModule | null;
158158
/** 0..1 */
159159
buildProgress: number;
160+
/** points in the snapshot being loaded, from its header (null until it arrives) */
161+
loadingCount: number | null;
162+
/** bytes of the vectors blob being fetched (count × dim × 4), or null */
163+
loadingBytes: number | null;
160164
health: MemoryCloudHealth | null;
161165
healthError: string | null;
162166
query: QueryState;
@@ -351,6 +355,8 @@ export function createMemoryCloudStore(deps: MemoryCloudDeps): UseBoundStore<Sto
351355
set({ status: 'loading', error: null, retryAt: null });
352356
try {
353357
const snap = await deps.fetchSnapshot({ signal: ctl.signal });
358+
if (ctl.signal.aborted) return;
359+
set({ loadingCount: snap.count, loadingBytes: snap.count * snap.dim * 4 });
354360
let bundle: VectorBundle;
355361
if (get().snapshot?.snapshotId === snap.snapshotId && get().vectors && get().engine) {
356362
bundle = { snapshot: snap, vectors: get().vectors! };
@@ -405,6 +411,8 @@ export function createMemoryCloudStore(deps: MemoryCloudDeps): UseBoundStore<Sto
405411
engine: null,
406412
trajectory: null,
407413
buildProgress: 0,
414+
loadingCount: null,
415+
loadingBytes: null,
408416
health: null,
409417
healthError: null,
410418
query: emptyQuery(),

‎client/src/features/visualisation/memoryTrajectory/__tests__/benchmark.test.ts‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
/**
2-
* Timing benchmark at the real snapshot scale (6000 x 384). The numbers are
3-
* logged, not asserted strictly: CI machines vary too much. The only hard
4-
* checks are that the build finishes and the query returns a sane result.
2+
* Timing benchmark at the real snapshot scale (MEMORY_CLOUD_SAMPLE 30 000 x
3+
* 384 since 2026-10-08; MEMORY_BENCH_N overrides it). The numbers are logged,
4+
* not asserted strictly: CI machines vary too much. The only hard checks are
5+
* that the build finishes and the query returns a sane result.
56
*/
67
import { describe, it, expect } from 'vitest';
78
import { buildHnsw, searchHnsw, exactTopK, recallAtK } from '../hnsw';
89
import { buildSearchTree } from '../tree';
910
import { layoutTree } from '../layout';
1011
import { clusteredVectors, perturbedQueries } from './fixtures';
1112

12-
describe('benchmark 6000 x 384', () => {
13+
const N = Number(process.env.MEMORY_BENCH_N ?? 30_000);
14+
15+
describe(`benchmark ${N} x 384`, () => {
1316
it('builds and runs one traced query', async () => {
14-
const vs = clusteredVectors(6000, 384, 40, 6000);
17+
const vs = clusteredVectors(N, 384, 40, N);
1518
const t0 = performance.now();
1619
const g = await buildHnsw(vs, { M: 12, efConstruction: 80, seed: 1 });
1720
const t1 = performance.now();
@@ -29,6 +32,7 @@ describe('benchmark 6000 x 384', () => {
2932
for (const q of queries) recall += recallAtK(searchHnsw(g, vs, q, 48, 10).top, exactTopK(vs, q, 10), 10) / queries.length;
3033

3134
const report = {
35+
n: N,
3236
buildMs: +(t1 - t0).toFixed(1),
3337
tracedQueryMs: +(t3 - t2).toFixed(2),
3438
traceEvents: r.trace.length,
@@ -44,5 +48,5 @@ describe('benchmark 6000 x 384', () => {
4448
expect(r.top.length).toBe(10);
4549
expect(ex.length).toBe(10);
4650
expect(recall).toBeGreaterThan(0.8);
47-
}, 300_000);
51+
}, 900_000);
4852
});

‎crates/visionclaw-memory-cloud/src/config.rs‎

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
//! process environment. Values that fail to parse fall back to the default;
55
//! numeric values outside their permitted range are clamped.
66
7-
/// Default total sample size (`MEMORY_CLOUD_SAMPLE`).
8-
pub const DEFAULT_SAMPLE_TOTAL: usize = 6000;
7+
/// Default total sample size (`MEMORY_CLOUD_SAMPLE`; operator decision
8+
/// 2026-10-08, it was 6000).
9+
pub const DEFAULT_SAMPLE_TOTAL: usize = 30_000;
910
/// Smallest permitted sample size.
1011
pub const MIN_SAMPLE_TOTAL: usize = 500;
11-
/// Largest permitted sample size.
12-
pub const MAX_SAMPLE_TOTAL: usize = 20_000;
12+
/// Largest permitted sample size (raised from 20 000 with the 30 000 default,
13+
/// leaving headroom).
14+
pub const MAX_SAMPLE_TOTAL: usize = 50_000;
1315
/// Default snapshot refresh interval in seconds (`MEMORY_CLOUD_REFRESH_SECS`).
1416
pub const DEFAULT_REFRESH_SECS: u64 = 900;
1517
/// Shortest permitted refresh interval in seconds.
@@ -241,7 +243,7 @@ mod tests {
241243
#[test]
242244
fn defaults_apply_when_unset() {
243245
let c = cfg(&[]);
244-
assert_eq!(c.sample_total, 6000);
246+
assert_eq!(c.sample_total, 30_000, "operator decision 2026-10-08");
245247
assert_eq!(c.refresh_secs, 900);
246248
assert_eq!(c.excluded.as_written(), vec!["personal-context"]);
247249
assert_eq!(c.embed_url, "http://xinference:9997/v1");
@@ -252,10 +254,19 @@ mod tests {
252254
fn numeric_values_are_clamped_and_garbage_defaults() {
253255
assert_eq!(
254256
cfg(&[("MEMORY_CLOUD_SAMPLE", "999999")]).sample_total,
255-
20_000
257+
50_000
256258
);
259+
assert_eq!(
260+
cfg(&[("MEMORY_CLOUD_SAMPLE", "30000")]).sample_total,
261+
30_000,
262+
"the default is inside the clamp"
263+
);
264+
assert_eq!(cfg(&[("MEMORY_CLOUD_SAMPLE", "6000")]).sample_total, 6000);
257265
assert_eq!(cfg(&[("MEMORY_CLOUD_SAMPLE", "12")]).sample_total, 500);
258-
assert_eq!(cfg(&[("MEMORY_CLOUD_SAMPLE", "lots")]).sample_total, 6000);
266+
assert_eq!(
267+
cfg(&[("MEMORY_CLOUD_SAMPLE", "lots")]).sample_total,
268+
DEFAULT_SAMPLE_TOTAL
269+
);
259270
assert_eq!(cfg(&[("MEMORY_CLOUD_SAMPLE", " 7000 ")]).sample_total, 7000);
260271
assert_eq!(cfg(&[("MEMORY_CLOUD_REFRESH_SECS", "5")]).refresh_secs, 60);
261272
assert_eq!(

‎docker-compose.unified.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ services:
133133
# sidecar on visionclaw_network. Empty RUVECTOR_PG_CONNINFO disables it
134134
# (data endpoints answer 503). Never put a password default here.
135135
RUVECTOR_PG_CONNINFO: ${RUVECTOR_PG_CONNINFO:-}
136-
MEMORY_CLOUD_SAMPLE: ${MEMORY_CLOUD_SAMPLE:-6000}
136+
MEMORY_CLOUD_SAMPLE: ${MEMORY_CLOUD_SAMPLE:-30000}
137137
MEMORY_CLOUD_REFRESH_SECS: ${MEMORY_CLOUD_REFRESH_SECS:-900}
138138
MEMORY_CLOUD_EXCLUDE_NAMESPACES: ${MEMORY_CLOUD_EXCLUDE_NAMESPACES:-personal-context}
139139
MEMORY_CLOUD_EMBED_URL: ${MEMORY_CLOUD_EMBED_URL:-http://xinference:9997/v1}
@@ -272,7 +272,7 @@ services:
272272
# sidecar on visionclaw_network. Empty RUVECTOR_PG_CONNINFO disables it
273273
# (data endpoints answer 503). Never put a password default here.
274274
RUVECTOR_PG_CONNINFO: ${RUVECTOR_PG_CONNINFO:-}
275-
MEMORY_CLOUD_SAMPLE: ${MEMORY_CLOUD_SAMPLE:-6000}
275+
MEMORY_CLOUD_SAMPLE: ${MEMORY_CLOUD_SAMPLE:-30000}
276276
MEMORY_CLOUD_REFRESH_SECS: ${MEMORY_CLOUD_REFRESH_SECS:-900}
277277
MEMORY_CLOUD_EXCLUDE_NAMESPACES: ${MEMORY_CLOUD_EXCLUDE_NAMESPACES:-personal-context}
278278
MEMORY_CLOUD_EMBED_URL: ${MEMORY_CLOUD_EMBED_URL:-http://xinference:9997/v1}

‎docs/XR-client.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
---
22
title: XR Client Architecture
33
doc_id: VC-XR
4-
version: 0.1.18
4+
version: 0.1.19
55
status: draft-for-ratification
66
verified_commit:
77
changelog:
8+
- "0.1.19 (2026-10-08): server sample 30 000 (ADR-2133 amendment). The frame budget still caps the cloud at 8 000 sprites; a 30 000 snapshot draws 8 000 (namespace-stratified, route and hit rows pinned) and the HUD Memory button reads 'Memory: <drawn> of <sampled>' whenever it draws fewer. Triangle budget unchanged. No invariant changed."
89
- "0.1.18 (2026-10-08): memory body at half its clear distance (ADR-2135 amendment, MEMORY_DISTANCE_FACTOR 0.5; the ×10 cloud now encloses both graphs) and the headset route lines at a tenth of the desktop thickness and brightness (XR_ROUTE_THICKNESS_SCALE, XR_ROUTE_GLOW_SCALE 0.1; rings and hit marks keep the desktop size). Operator decision. Triangle budget unchanged. No invariant changed."
910
- "0.1.17 (2026-10-08): always separate, memory ×10, typed search (ADR-2135 amendment, ADR-2136). The separation slider is gone from the HUD Layout page and the desktop (separation_control.gd deleted); the triangle is permanent at the fixed SEPARATION 190 derived from the live graph radii. The memory cloud is MEMORY_BODY_SCALE 10 graphs wide on the memory vertex's ray, clear of both graphs (TriangleFrame::memory_centre); route tubes, beads and rings grow with it (cloud-local), guide dots grow to half the answer ring at the far end, the hover label, its lift and its reach ×10. Memory Search gains a press-fire on-screen keyboard (onscreen_keyboard.gd, in place of the lists, ≤ 532 px); a typed query searches globally. The query response carries query.position (the snapshot's PCA basis), and the headset route runs query point → sidecar top-k in rank order with the answer ring on the top hit. Triangle budget unchanged (scale is free). No invariant changed."
1011
- "0.1.16 (2026-10-07): HUD Graph Separation control (ADR-2135 in the headset) — Sep −/slider/value/Sep + share the Layout Mode row (page stays 529 px); separation_control.gd writes graphSeparationX through the physics PUT at ≤ 4 Hz while dragging plus a final write on release; read-back moves the slider. Memory search from the headset — Query page Graph Query / Memory Search modes; presets POST /api/memory-cloud/query (NIP-98) and draw a sidecar top-k route (sampled hits in rank order, labelled as such) through the same MemoryRoute gate; a hit press retargets the guide cue; shared query-response fixture pins the headset parser to the server's wire types; benchmark route_source=query. No invariant changed."

‎docs/adr/ADR-2008-dev-image-recompiles.md‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ implementation_status: partial
77
activation_status: live
88
supersedes: []
99
superseded_by: []
10-
verified_commit: a9d587976591cdaedcd0c72e85febd9c6d61a97a
10+
verified_commit: 67086c29ebce293d78f77d7bb668ae9ec0dac765
1111
verified_paths: [scripts/dev-entrypoint.sh, docker-compose.unified.yml, scripts/rust-backend-wrapper.sh, scripts/lib/dev-inputs.sh, scripts/launch.sh]
1212
owner: jjohare
1313
review_trigger: a dev-loop turnaround that makes on-start compilation intolerable, or a move to pre-baked dev binaries by default
@@ -202,3 +202,9 @@ fires. `verified_commit` moved to the CI-repair commit.
202202
**Deploy path.** `./scripts/launch.sh redeploy dev` publishes, then runs `supervisorctl -c /app/supervisord.dev.conf restart rust-backend` in the running container; it refuses, naming `up dev`, when the compose config drifted. `up` on a running container with newer build inputs now redeploys instead of stopping and starting the container. Pre-existing defect fixed alongside: `up` against a running healthy container never applied compose changes. Both app services carry `visionclaw.compose-hash` (sha256 of the service's resolved `docker compose config`, computed with the label empty), and `up` recreates on mismatch. `needs_recompile` reads this record's build-input inventory and counts a redeploy since start.
203203

204204
**Verification.** `bash scripts/tests/test-dev-inputs.sh`: 83 pass; against the parent's compose file the mount checks fail. `SECRETS_ROOT=<main checkout>` runs the secret scan over the real checkout's mount sources (clean). `python3 scripts/tests/test_dev_launcher.py`: 2 pass (8 consecutive runs). `cargo test -p visionclaw-integration-tests --test dev_build_inputs` 18/18 and `--test prod_ingress` 9/9. No container operation ran. Activation needs a recreate for the new mounts and label (`launch.sh up dev` does it); `LIVE=1 bash scripts/tests/test-dev-inputs.sh` then checks the running container read-only.
205+
206+
## Re-verification — 2026-10-08 at 67086c29e
207+
208+
`docker-compose.unified.yml` changed only in the memory-cloud sample default
209+
(`MEMORY_CLOUD_SAMPLE` 6000 → 30000, ADR-2133 amendment; `docker-compose.unified.yml:136`, `:275`). Nothing
210+
this decision governs moved, so it holds as written.

‎docs/adr/ADR-2027-three-deployment-profiles.md‎

Lines changed: 7 additions & 1 deletion

0 commit comments

Comments
 (0)