localModels.embedding.parallel is the single-source declaration for how many embedding requests may be in flight — the leaf exists so one value applies whichever provider serves the lane (LM Studio, Ollama, llama.cpp). Nothing on the dispatch path honours it.
for (let offset = 0; offset < texts.length; offset += chunkSize) {
result = awaitthis.#enqueueOpenAiCompatiblePost(chunk, {...})
}
One POST is awaited before the next is issued. Measured on a live external tenant deployment 2026-08-20 (read-only), the provider's slot log shows ~20 consecutive launch → release → launch transitions with never two tasks in flight, slots selected round-robin by LRU:
387.04.023 launch id 1 task 9776
387.04.528 release id 1 task 9776 113 tok
387.04.537 launch id 3 task 9778
387.07.412 release id 3 task 9778 640 tok
The four slots are a rotation, not concurrency. The provider queues — it is not refusing work, it is never given any to overlap.
Two consequences on that plane:
A single long input occupies the entire lane. One 13,725-token chunk consumed a whole 5-minute slice budget while three slots idled. Cost on this lane fits ∝ n^1.58 (≈400 tokens ≈ 1 s; 13,725 ≈ 268 s), so one long input is worth hundreds of short ones — and currently blocks every one of them.
The capacity is demonstrably there. The same lane sustained ~19 embeddings/min on 100–1,400-token inputs within the same window.
This is the only consumer of the declared parallelism on this path, and it uses it to compute a request width, not a concurrency. Its stated purpose is to leave one slot free so other traffic can interleave, and that purpose is correct — see the 2026-08-21 amendment below. An earlier revision of this paragraph claimed a client cannot reserve a slot by sending fewer inputs; the provider's capacity unit is a TASK, so it can. What is wrong is the UNIT the clamp is spent in: headroom expressed as a request width consumes the same budget concurrency needs, so a lane declaring four slots offered one request at a time.
The reserved fraction is also accidental: parallel − 1 reserves 25% at four slots, 50% at two, 6% at sixteen. And the clamp only ever fires downward — with parallel <= 1 it falls through unclamped — so its entire effect is to reduce capacity on the multi-slot lanes it was written for.
Separately, the sweep's own concurrency is not a settable leaf at all (#17158), so a deployment cannot raise in-flight width from the outside either.
The Fix
Bound in-flight requests by localModels.embedding.parallel and let the provider schedule. Delete the slotHeadroomWidth clamp: request width comes from the durability contract, concurrency comes from the declared parallelism, and the two stop being conflated. If interleaving headroom for interactive probes is a real requirement, it belongs where that traffic is admitted — not as an unstated capacity tax on batch work.
The existing per-chunk yield point and its forward-progress guarantee (at least one chunk lands per acquisition) must survive: concurrency changes how many requests are outstanding, never whether a reached checkpoint is durable.
⚠️ AMENDED 2026-08-20 — what "must survive" actually costs, verified before implementing
That sentence was one line and it is the whole difficulty. Five properties of this loop are stated in terms of a CONTIGUOUS PREFIX, and fan-out breaks the arithmetic they rest on. Read at TextEmbeddingService.mjs:1991-1998:
completedChunkCount * chunkSize is a count multiplied by a width. It identifies a span only because completions arrive in order from offset 0 — which the production comment states outright: "Completed chunks are full-width by the same boundary argument the yield error makes (only a batch's final chunk can be short…), so the expected count derives from what was SENT."
With N requests outstanding, the completed set can be non-contiguous. Chunk 0 and chunk 2 may succeed while chunk 1 fails. Then completedChunkCount = 2, completedTextCount = 2 × chunkSize, and data holds indices from chunks 0 and 2 — a hole inside the claimed span and entries beyond it.
The consequence is not a mis-binding; it is worse than that.toOrderedEmbeddings's positional-binding guard fails closed, so the catch swallows and the error "travels uncarried". So concurrency does not corrupt vectors — it silently disables work conservation, which is precisely the regression the carry was added to fix. Every fan-out failure re-purchases the whole completed set, and nothing reports it: the lane just runs slower forever.
The five prefix-dependent properties, each needing an explicit answer rather than inheritance:
property
today's basis
what fan-out does to it
carried completedTextCount
count × width
non-contiguous completion makes the product name the wrong span
several spans may be in flight; a timeout naming "the request" names one of N
data ordering
append order == input order
append order is completion order; the final toOrderedEmbeddings already re-sorts by index, so this one survives
So the implementation is a representation change, not a wrapper. The carry must become explicitly indexed — the set of completed spans, not a count — and the guard must distinguish "nothing landed" from "something landed non-contiguously", because those are different facts and only one of them is a reason to drop the carry.
Discovered by reading the carry arithmetic before writing code rather than after. Recorded here because an implementation that added concurrency and left count × width in place would pass a naive concurrency arm, ship a silent conservation regression, and present as a throughput fix.
Contract Ledger Matrix
AMENDED 2026-08-21 (second pass) on @neo-gpt's reopened P1. What each declared budget means, so the specs are anchored to the AC rather than to whichever number made an arm observable:
parallel = 4 — batch work peaks at 3; batch plus a concurrent interactive post peaks at 4. This is the reservation, and it is the case the ticket's headroom contract is about.
parallel = 5 — the exact-peak-4 fan-out arm. Budget 5 is required to observe a four-wide batch peak because of the reservation, so that arm measures the worker-pool mechanism, not the headroom rule.
AMENDED 2026-08-21 on @neo-gpt's P1 (PR #17433). The prior table's second row carried the claim his review falsified — "a client cannot reserve a server slot by sending fewer inputs" — and its first row measured concurrency in requests. Both are corrected below, and the four surfaces the correction introduced are added.
Target Surface
Source of Authority
Proposed Behavior
Fallback
Docs
Evidence
resolveEmbeddingTaskBudget (new, pure)
localModels.embedding.parallel
resolves the declared capacity as a task count and THROWS on a non-positive-integer
none — the leaf's own default covers an absent env var, so a bad value here is a config defect
—
a silent fallback to 1 reports healthy while ignoring what the deployment declared
admission unit — resolveDispatchPlan (new, pure)
the task budget + openAiCompatible.batchEmbeddingChunkSize
one multi-input POST expands to one task per input, so offered work is concurrency × width; both resolve together against the budget
budget 1 ⇒ configured width and concurrency 1, byte-identical to pre-change behaviour
cloud-deployment docs
at parallel=4, width=5 the prior head offered 20 tasks against 4 slots
interactive headroom
the queue's admission rule, not the per-call plan
BATCH posts admitted only to budget - 1; INTERACTIVE posts may use the whole budget
zero tasks in flight always admits, so budget 1 and an oversized post are both untouched
—
a per-call reservation cannot hold it: two batch callers each satisfy their own and jointly fill the budget, measured at 4/4 with no slot left
slotHeadroomWidth
—
deleted
—
—
its mechanism (computing a width) could not express the constraint; its intent (headroom) was correct and is preserved above. It also fell through unclamped at parallel <= 1
worker pool — #drainOpenAiCompatiblePostQueue
the task budget, as a ceiling only
N bounded workers replacing a single-worker boolean guard; each selects through the unchanged interactive-first index
—
—
its own docblock said it ran posts "one at a time", so removing the width clamp alone changed nothing
prefix / drop receipt — resolveCompletedPrefix
the completed span set
returns the longest contiguous carryable prefix plus droppedChunkCount; the count rides both thrown envelopes and the operation record, set unconditionally
no completions ⇒ {0, 0, 0}, and the failure travels uncarried
—
completedChunkCount * chunkSize named a span only while completions arrived in order; the binding guard then failed closed and work conservation switched off silently
error precedence
the drain outcome
a provider failure or caller abort outranks a cooperative yield observed earlier in the same batch, evaluated after the drain
no failure ⇒ the yield reports
—
reporting the polite version would hand the caller a resumable checkpoint for a lane that is actually broken
per-span lease yield
orchestrator.heavyMaintenance.maxActiveHoldMs
consulted between span admissions, keyed on the carryable prefix rather than any completion
completedChunkCount > 0 still gates the first yield
—
a span landing after a hole is not durable, so treating it as forward progress lets an acquisition yield having banked nothing
failure attribution
what was SENT
each outstanding request carries its own failedTextOffset / failedTextCount; the span travels with the error rather than being re-derived from a loop variable that has moved on
—
—
assigning a multi-input timeout to the first member is how a neighbour inherits strikes
Decision Record impact
aligned-with ADR 0014 — the two-lane provider split is unchanged; this alters only how many requests the client keeps outstanding on the embedding lane.
Acceptance Criteria
RED-PROOF, concurrency: concurrency is observed by OVERLAP against a recording provider stub, never by counting calls — a sequential loop makes the same number of calls. The exact peak is budget-dependent because of the interactive reservation, and an earlier revision of this AC said parallel = 4 yields at least four concurrent requests, which the reservation makes false: at parallel = 4batch work peaks at 3, and batch plus a concurrent interactive post peaks at 4. The four-wide batch fan-out is observable at parallel = 5, and that arm measures the worker pool rather than the headroom rule. Both peaks are asserted exactly, never as a lower bound.
SILENT ARM: with parallel = 1 (or unset), behaviour is byte-identical to today — one request outstanding, same ordering. Without this arm a change that always fans out passes the arm above.
RED-PROOF, head-of-line: one slow input does not delay unrelated pending inputs. Asserted with a stub whose first request resolves last: the later inputs complete before it, and the assembled output stays in input order.
The declared value is the only authority.slotHeadroomWidth is gone, and no site derives a width or a concurrency from parallel by arithmetic. A fixture asserts that changing parallel changes observed concurrency, so it is not a constant wearing a leaf's name.
Forward progress is preserved under concurrency. A lease yield mid-flight still lands every completed request durably; a repeatedly-yielding acquisition never re-embeds a prefix it already completed. Asserted on the yield path, not only the happy path.
RED-PROOF, non-contiguous conservation — the arm this ticket was missing. With four outstanding requests where an EARLY one fails and LATER ones succeed, the carried result still reports every completed span. Asserted against today's tree it must FAIL, and it must fail by carrying nothing rather than by mis-binding — the positional guard fails closed, so the defect is a silent loss of work conservation, not a wrong vector. An arm that only checks "no wrong vectors" passes on the broken shape.
completedTextCount is no longer a product. No site derives a completed span from count × width; a fixture with a deliberate hole in the completed set proves the carry names the real spans. This is the representation change the ticket rests on, so it is asserted directly rather than implied by the arm above.
"Nothing landed" and "something landed out of order" are distinguishable. The first is a reason to drop the carry; the second is not, and today they are the same code path. Asserted by observing the carry survive a non-contiguous completion.
Failure attribution survives fan-out. A single failing request among four outstanding attributes its strikes to its own inputs only. Asserted with three concurrent successes and one failure.
Ordering is contract, not accident: outputs map to inputs by index regardless of completion order, asserted with deliberately inverted completion.
Out of Scope
The corpus chunk ceiling. 16,384 tokens per chunk stays — source-code chunks must remain semantically whole for retrieval.
Provider memory or CPU allocation. This ticket makes the existing allocation reachable; it does not ask for more.
Making the sweep's concurrency configurable — that is #17158, and it composes with this rather than being fixed by it.
Retiring the compensating layers this unblocks. Deliberately deferred: which of them still fire is only decidable once the lane runs at its declared width, and that census is governed by the parent epic.
Avoided Traps
Do not widen the request instead. A wider POST with a sequential loop changes little and would present as a fix that did nothing.
Do not add a second concurrency authority. The parallelism leaf exists so one value spans providers; a new maxConcurrentEmbedRequests beside it recreates the drift this ticket removes.
Do not read the idle slots as a provider limitation. The provider queues and idles when nothing arrives; the bound is client-side.
Do not assert concurrency by call count. A sequential loop makes N calls too. The arm has to observe overlapping lifetimes or it proves nothing.
Related
Parent: #17411. Composes with #17158 (sweep concurrency not settable) and #16972 (batch retried at identical size). Outcome bar: D#17136.
tobiu referenced in commit 330ae0e - "feat(ai): the declared parallelism becomes a concurrency, and the carry stops being arithmetic (#17412) on Aug 21, 2026, 2:31 AM
tobiu referenced in commit 5dd95d4 - "fix(ai): the capacity unit is a task, so width and concurrency share one budget (#17412) on Aug 21, 2026, 2:31 AM
tobiu referenced in commit b7aa271 - "test(ai): four concurrency scenarios pure prefix arithmetic cannot reach (#17412) on Aug 21, 2026, 2:31 AM
Context
localModels.embedding.parallelis the single-source declaration for how many embedding requests may be in flight — the leaf exists so one value applies whichever provider serves the lane (LM Studio, Ollama, llama.cpp). Nothing on the dispatch path honours it.The Problem
TextEmbeddingService.mjs:1930iterates provider requests sequentially:for (let offset = 0; offset < texts.length; offset += chunkSize) { result = await this.#enqueueOpenAiCompatiblePost(chunk, {...}) }One POST is awaited before the next is issued. Measured on a live external tenant deployment 2026-08-20 (read-only), the provider's slot log shows ~20 consecutive
launch → release → launchtransitions with never two tasks in flight, slots selected round-robin by LRU:The four slots are a rotation, not concurrency. The provider queues — it is not refusing work, it is never given any to overlap.
Two consequences on that plane:
∝ n^1.58(≈400 tokens ≈ 1 s; 13,725 ≈ 268 s), so one long input is worth hundreds of short ones — and currently blocks every one of them.The Architectural Reality
Directly above the loop:
slotHeadroomWidth = Number.isInteger(embeddingParallel) && embeddingParallel > 1 ? embeddingParallel - 1 : configuredChunkSize, chunkSize = Math.min(configuredChunkSize, slotHeadroomWidth),This is the only consumer of the declared parallelism on this path, and it uses it to compute a request width, not a concurrency. Its stated purpose is to leave one slot free so other traffic can interleave, and that purpose is correct — see the 2026-08-21 amendment below. An earlier revision of this paragraph claimed a client cannot reserve a slot by sending fewer inputs; the provider's capacity unit is a TASK, so it can. What is wrong is the UNIT the clamp is spent in: headroom expressed as a request width consumes the same budget concurrency needs, so a lane declaring four slots offered one request at a time.
The reserved fraction is also accidental:
parallel − 1reserves 25% at four slots, 50% at two, 6% at sixteen. And the clamp only ever fires downward — withparallel <= 1it falls through unclamped — so its entire effect is to reduce capacity on the multi-slot lanes it was written for.Separately, the sweep's own concurrency is not a settable leaf at all (#17158), so a deployment cannot raise in-flight width from the outside either.
The Fix
Bound in-flight requests by
localModels.embedding.paralleland let the provider schedule. Delete theslotHeadroomWidthclamp: request width comes from the durability contract, concurrency comes from the declared parallelism, and the two stop being conflated. If interleaving headroom for interactive probes is a real requirement, it belongs where that traffic is admitted — not as an unstated capacity tax on batch work.The existing per-chunk yield point and its forward-progress guarantee (at least one chunk lands per acquisition) must survive: concurrency changes how many requests are outstanding, never whether a reached checkpoint is durable.
⚠️ AMENDED 2026-08-20 — what "must survive" actually costs, verified before implementing
That sentence was one line and it is the whole difficulty. Five properties of this loop are stated in terms of a CONTIGUOUS PREFIX, and fan-out breaks the arithmetic they rest on. Read at
TextEmbeddingService.mjs:1991-1998:const completedTextCount = completedChunkCount * chunkSize, embeddings = toOrderedEmbeddings(data, completedTextCount);completedChunkCount * chunkSizeis a count multiplied by a width. It identifies a span only because completions arrive in order from offset 0 — which the production comment states outright: "Completed chunks are full-width by the same boundary argument the yield error makes (only a batch's final chunk can be short…), so the expected count derives from what was SENT."With N requests outstanding, the completed set can be non-contiguous. Chunk 0 and chunk 2 may succeed while chunk 1 fails. Then
completedChunkCount = 2,completedTextCount = 2 × chunkSize, anddataholds indices from chunks 0 and 2 — a hole inside the claimed span and entries beyond it.The consequence is not a mis-binding; it is worse than that.
toOrderedEmbeddings's positional-binding guard fails closed, so thecatchswallows and the error "travels uncarried". So concurrency does not corrupt vectors — it silently disables work conservation, which is precisely the regression the carry was added to fix. Every fan-out failure re-purchases the whole completed set, and nothing reports it: the lane just runs slower forever.The five prefix-dependent properties, each needing an explicit answer rather than inheritance:
completedTextCountcount × widthcompletedChunkCount > 0)failedTextOffset/failedTextCount)dataorderingtoOrderedEmbeddingsalready re-sorts byindex, so this one survivesSo the implementation is a representation change, not a wrapper. The carry must become explicitly indexed — the set of completed spans, not a count — and the guard must distinguish "nothing landed" from "something landed non-contiguously", because those are different facts and only one of them is a reason to drop the carry.
Discovered by reading the carry arithmetic before writing code rather than after. Recorded here because an implementation that added concurrency and left
count × widthin place would pass a naive concurrency arm, ship a silent conservation regression, and present as a throughput fix.Contract Ledger Matrix
AMENDED 2026-08-21 (second pass) on @neo-gpt's reopened P1. What each declared budget means, so the specs are anchored to the AC rather than to whichever number made an arm observable:
parallel = 4— batch work peaks at 3; batch plus a concurrent interactive post peaks at 4. This is the reservation, and it is the case the ticket's headroom contract is about.parallel = 5— the exact-peak-4 fan-out arm. Budget 5 is required to observe a four-wide batch peak because of the reservation, so that arm measures the worker-pool mechanism, not the headroom rule.AMENDED 2026-08-21 on @neo-gpt's P1 (PR #17433). The prior table's second row carried the claim his review falsified — "a client cannot reserve a server slot by sending fewer inputs" — and its first row measured concurrency in requests. Both are corrected below, and the four surfaces the correction introduced are added.
resolveEmbeddingTaskBudget(new, pure)localModels.embedding.parallelresolveDispatchPlan(new, pure)openAiCompatible.batchEmbeddingChunkSizeconcurrency × width; both resolve together against the budget1⇒ configured width and concurrency 1, byte-identical to pre-change behaviourparallel=4, width=5the prior head offered 20 tasks against 4 slotsbudget - 1; INTERACTIVE posts may use the whole budget1and an oversized post are both untouchedslotHeadroomWidthparallel <= 1#drainOpenAiCompatiblePostQueueresolveCompletedPrefixdroppedChunkCount; the count rides both thrown envelopes and the operation record, set unconditionally{0, 0, 0}, and the failure travels uncarriedcompletedChunkCount * chunkSizenamed a span only while completions arrived in order; the binding guard then failed closed and work conservation switched off silentlyorchestrator.heavyMaintenance.maxActiveHoldMscompletedChunkCount > 0still gates the first yieldfailedTextOffset/failedTextCount; the span travels with the error rather than being re-derived from a loop variable that has moved onDecision Record impact
aligned-with ADR 0014— the two-lane provider split is unchanged; this alters only how many requests the client keeps outstanding on the embedding lane.Acceptance Criteria
parallel = 4yields at least four concurrent requests, which the reservation makes false: atparallel = 4batch work peaks at 3, and batch plus a concurrent interactive post peaks at 4. The four-wide batch fan-out is observable atparallel = 5, and that arm measures the worker pool rather than the headroom rule. Both peaks are asserted exactly, never as a lower bound.parallel = 1(or unset), behaviour is byte-identical to today — one request outstanding, same ordering. Without this arm a change that always fans out passes the arm above.slotHeadroomWidthis gone, and no site derives a width or a concurrency fromparallelby arithmetic. A fixture asserts that changingparallelchanges observed concurrency, so it is not a constant wearing a leaf's name.completedTextCountis no longer a product. No site derives a completed span fromcount × width; a fixture with a deliberate hole in the completed set proves the carry names the real spans. This is the representation change the ticket rests on, so it is asserted directly rather than implied by the arm above.Out of Scope
Avoided Traps
maxConcurrentEmbedRequestsbeside it recreates the drift this ticket removes.Related
Parent: #17411. Composes with #17158 (sweep concurrency not settable) and #16972 (batch retried at identical size). Outcome bar: D#17136.