From e8142730d54a7e6dc8e8a1d24443a93529a77c49 Mon Sep 17 00:00:00 2001 From: Ronald Tse Date: Tue, 1 Sep 2026 08:17:35 +0200 Subject: [PATCH] feat: Range-resumable model downloads (TODO.client-work 09) Dropped connections resume from the received byte offset (up to 5 attempts); servers that ignore Range restart cleanly instead of corrupting the artifact. --- TODO.client-work/08-neural-demo-island.md | 5 ++ TODO.client-work/09-resumable-downloads.md | 5 ++ TODO.client-work/10-api-batch-infer.md | 5 ++ .../11-confidence-highlighting.md | 5 ++ TODO.client-work/12-e3-browser-bench.md | 5 ++ src/ml/imf/registry.ts | 47 ++++++++++++++----- 6 files changed, 61 insertions(+), 11 deletions(-) create mode 100644 TODO.client-work/08-neural-demo-island.md create mode 100644 TODO.client-work/09-resumable-downloads.md create mode 100644 TODO.client-work/10-api-batch-infer.md create mode 100644 TODO.client-work/11-confidence-highlighting.md create mode 100644 TODO.client-work/12-e3-browser-bench.md diff --git a/TODO.client-work/08-neural-demo-island.md b/TODO.client-work/08-neural-demo-island.md new file mode 100644 index 0000000..10cbbf8 --- /dev/null +++ b/TODO.client-work/08-neural-demo-island.md @@ -0,0 +1,5 @@ +# 08-neural-demo-island + +Live neural inference demo on interscript.org: model picker with tier badges + sizes, progress bar during resolve, streaming output, low-confidence highlighting (11), cache/offline status + +Status: implementing (2026-09-01). diff --git a/TODO.client-work/09-resumable-downloads.md b/TODO.client-work/09-resumable-downloads.md new file mode 100644 index 0000000..db8c75b --- /dev/null +++ b/TODO.client-work/09-resumable-downloads.md @@ -0,0 +1,5 @@ +# 09-resumable-downloads + +Range-based resume for flaky networks: interrupted zip downloads continue from the received byte offset instead of restarting 250MB + +Status: DONE (2026-09-01) (2026-09-01). diff --git a/TODO.client-work/10-api-batch-infer.md b/TODO.client-work/10-api-batch-infer.md new file mode 100644 index 0000000..bf7fd1b --- /dev/null +++ b/TODO.client-work/10-api-batch-infer.md @@ -0,0 +1,5 @@ +# 10-api-batch-infer + +POST /v1/infer/batch on the API worker: N inputs per request, sequential Modal upstream, per-item error isolation + +Status: implementing (2026-09-01). diff --git a/TODO.client-work/11-confidence-highlighting.md b/TODO.client-work/11-confidence-highlighting.md new file mode 100644 index 0000000..f06e647 --- /dev/null +++ b/TODO.client-work/11-confidence-highlighting.md @@ -0,0 +1,5 @@ +# 11-confidence-highlighting + +Demo UI surfaces onConfidence gaps as output highlighting (low-confidence spans marked) + +Status: implementing (2026-09-01). diff --git a/TODO.client-work/12-e3-browser-bench.md b/TODO.client-work/12-e3-browser-bench.md new file mode 100644 index 0000000..efb99da --- /dev/null +++ b/TODO.client-work/12-e3-browser-bench.md @@ -0,0 +1,5 @@ +# 12-e3-browser-bench + +The browser benchmark cell (paper C): Playwright timing of the real demo page — load, cold/warm cache, decode latency on WASM + +Status: implementing (2026-09-01). diff --git a/src/ml/imf/registry.ts b/src/ml/imf/registry.ts index 9b893f3..33c86c3 100644 --- a/src/ml/imf/registry.ts +++ b/src/ml/imf/registry.ts @@ -185,24 +185,49 @@ async function evictStaleEntries( } } -/** Fetch with progress via a streamed body (TODO.client-work 03). */ +/** Fetch with progress via a streamed body, resuming across dropped + * connections with Range requests (TODO.client-work 03/09): a flaky + * network continues from the received byte offset instead of + * restarting a 250MB artifact. */ async function fetchWithProgress( url: string, onProgress?: (fraction: number, bytes: number) => void, ): Promise { if (!onProgress) return new Uint8Array(await (await fetch(url)).arrayBuffer()) - const res = await fetch(url) - if (!res.ok || !res.body) throw new RegistryError(`fetch failed: ${url} -> ${res.status}`) - const total = Number(res.headers.get("content-length") ?? 0) - const reader = res.body.getReader() const chunks: Uint8Array[] = [] let received = 0 - for (;;) { - const { done, value } = await reader.read() - if (done) break - chunks.push(value) - received += value.length - onProgress(total > 0 ? received / total : 0, received) + let total = 0 + for (let attempt = 0; attempt < 5; attempt++) { + let res: Response + try { + res = await fetch(url, received > 0 ? { headers: { range: `bytes=${received}-` } } : {}) + } catch { + continue + } + if (!res.ok && res.status !== 206) throw new RegistryError(`fetch failed: ${url} -> ${res.status}`) + if (received === 0) total = Number(res.headers.get("content-length") ?? 0) + else if (res.status !== 206) { + // server ignored Range: restart cleanly rather than corrupt + chunks.length = 0 + received = 0 + total = Number(res.headers.get("content-length") ?? 0) + } + const reader = res.body?.getReader() + if (!reader) continue + let dropped = false + for (;;) { + try { + const { done, value } = await reader.read() + if (done) break + chunks.push(value) + received += value.length + onProgress(total > 0 ? Math.min(1, received / total) : 0, received) + } catch { + dropped = true + break + } + } + if (!dropped) break } const out = new Uint8Array(received) let offset = 0