QVAC-18183 feat[api]: non-inference migrations + decorated-promise requestId#2060
Merged
simon-iribarren merged 8 commits intoMay 15, 2026
Conversation
…questId
Migrate the last three pre-M3 long-running handlers onto the
`RequestRegistry` and expose `requestId` synchronously on the
Promise-shaped public-API calls.
Deliverables (per the M3c implementation brief):
1. **`rag`** (kind `"rag"`) — `server/rpc/handlers/rag.ts` now opens
`await using ctx = registry.begin({ kind: "rag" })` for every
workspace-bound op (`ingest`, `saveEmbeddings`, `reindex`).
`rag-operation-manager.ts` is rewritten around a small
workspace → requestId map (`get/set/clearActiveRagRequest`) plus a
`cancelAllRagOperations` shutdown sweep. The pre-existing
`activeOperations` / `registerRagOperation` /
`unregisterRagOperation` / `cancelRagOperation` API is removed —
nothing outside the manager touched it.
2. **`downloadAsset`** (kind `"downloadAsset"`) — handler wraps the
resolve flow in `registry.begin(...)` and threads `ctx.signal` plus
a `DownloadHooks.requestBinding` into `resolveModelPath(...)` /
`resolveModelPathWithStats(...)`. In `download-manager.ts`,
`startOrJoinDownload` now accepts a per-subscriber
`SubscriberRequestBinding` ({ signal, scope, requestId }); the
content-addressed dedup keyed on `downloadKey` is preserved (two
callers requesting the same artifact still share one transfer) but
each subscriber attaches an abort listener that rejects its
promise alone, and the new `maybeCancelTransfer(transfer)` helper
aborts the underlying transfer only when the **last** subscriber
leaves. `scope.defer(() => removeSubscriber(...))` is the
safety-net cleanup for handler-exit paths that don't go through
the abort listener.
3. **`loadModel`** (kind `"loadModel"`) — handler wraps the
download-then-load flow in `registry.begin(...)`, threads
`ctx.signal` into the resolver chain, and gates the
`plugin.createModel(...)` / `model.load(false)` call on
`ctx.signal.aborted` before and after. Load phase is soft-cancel
only — the addon's load path doesn't accept a signal today;
documented as a follow-up.
4. **Decorated-promise `requestId`** — new helper
`packages/sdk/utils/decorate-promise.ts` (`Object.assign`-based,
intentionally not a prototype extension). `loadModel(...)` and
`downloadAsset(...)` return `Promise<string> & { requestId: string }`
so callers can grab `op.requestId` synchronously and fire
`cancel({ requestId: op.requestId })` before the network round-trip
resolves. The unwrap chain is preserved: `await loadModel(...)`
still returns the model id, pinned by
`test/unit/utils/decorate-promise.test.ts`.
5. **Cursor rules** — `request-lifecycle-primitives.mdc` flips the
migration table to mark M3b/M3c shipped, documents the decorated-
promise pattern, and adds the "what's on the registry today"
dispatch-level truth table. `docs/request-lifecycle-system.mdc`
flips the M3c roadmap row.
§4 Open decisions (resolved):
- **Decision A — cancel error class:** A.1, reuse
`InferenceCancelledError` (52419) across all non-inference
handlers. Zero new public-API surface. The naming oddity is filed
as a potential rename follow-up.
- **Decision B — RAG workspace-level admission:** B.2, dispatcher-
level pre-emption in `rag.ts`. Before `registry.begin({ kind: "rag" })`,
the dispatcher walks the workspace→requestId map and calls
`registry.cancel({ requestId: prev })` for any prior in-flight op
on the same workspace. No new field on `ConcurrencyPolicy`. The
cancel-prior → begin-new order is load-bearing — reversing it
would cancel the just-installed context.
- **`resolveModelPath` signal threading:** `signal: AbortSignal`
parameter, not a full `ctx: RequestContext`. The resolver doesn't
need the context — just the signal — so the narrower parameter is
the idiomatic shape across all call sites
(`http.ts`, `hyperdrive.ts`, `registry.ts`, `resolve-session.ts`).
`cancelHandler.ts`'s `case "downloadAsset"` and `case "rag"` arms
stay intact (M1 compat contract). They now emit a deprecation log
line and route through `registry.cancel({ requestId })` rather than
the legacy direct paths. M3d retires the dispatcher entirely.
Tests added:
- `test/unit/utils/decorate-promise.test.ts` — sync metadata access,
unwrap-to-T, rejection propagation, in-place identity, chain
integrity.
- `test/unit/download-manager-subscriber.test.ts` — dedup preserved,
per-subscriber cancel doesn't affect siblings, last-subscriber
leaving aborts the transfer, `scope.defer` safety net, cancel
error carries `requestId`.
- `test/unit/rag-workspace-preempt.test.ts` — workspace map
get/set/clear semantics + the "still mine?" guard against stale
scope-unwind clears.
- `test/unit/request-id-wire.test.ts` — `loadModel` / `downloadAsset` /
`rag` request schemas preserve `requestId` on the wire and treat
it as optional for legacy clients.
`bun lint`, `bun run typecheck`, `bun test test/unit/` (additive
change — pre-existing unrelated `tts-multicast` test failure on this
worktree is from missing `dist/` for the `#rpc` package alias; not
introduced here).
- Soften M3b roadmap rows to "in review (tetherto#2058)" in request-lifecycle primitives + system docs until tetherto#2058 merges (avoids briefly advertising M3b as shipped while M3c lands first). - Promote the "no in-flight match" log in cancelHandler.ts's `case "request"` arm from debug to info: the decorated-promise pattern makes post-settle Stop-button cancels a common, user-visible code path and needs to surface without lowering the log level. - Route cancelTransfer's orphan-subscriber settle through removeSubscriber(...) so the last-subscriber teardown rule is enforced in one place (drops a redundant inline abort guard).
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Contributor
Contributor
QVAC E2E —
|
Contributor
QVAC E2E —
|
Contributor
Victor-Rodzko
previously approved these changes
May 14, 2026
…etherto#2060 review feedback) Removed the "Migration Roadmap" table, "M1/M2/M3a-d" milestone labels, planning-brief decision references (Decision A/B.2, D1/D2), workspace-local paths (`tasks/release-0.11.0-planning/...`, `pitch-3-decisions.md`), and "in review tetherto#2058" forward-references from the request-lifecycle cursor rules and the matching code comments. The dispatch-level "What's on the registry today" truth table remains and now reflects only the kinds actually routed through the registry on this branch.
Victor-Rodzko
approved these changes
May 14, 2026
opaninakuffo
approved these changes
May 14, 2026
NamelsKing
approved these changes
May 15, 2026
Contributor
Author
|
/review |
Contributor
Tier-based Approval Status |
simon-iribarren
added a commit
to simon-iribarren/qvac
that referenced
this pull request
May 15, 2026
Absorbed M3c (tetherto#2060) merge from main; reconciled rule files (union of registry-routed kinds in 'What's on the registry today' table — kept main's completion/loadModel/downloadAsset/rag rows and added M3b's embeddings/transcribe/translate/finetune rows for 8 total), schemas / plugin manifests (kept M3b's `requestId` field and `cancel: { scope, hard }` declarations intact), and ops comment cleanup (took main's trim where prose conflicted; M3b's implementation logic was identical on both sides). The "Implementation Files" table in .cursor/rules/sdk/docs/request-lifecycle-system.mdc is the union of both branches' rows, with M3b's expanded descriptions kept (they're a strict superset of main's). Concurrency-policy paragraphs in both rule files keep M3b's wording since it's the more informative version covering the actually-migrated kinds. Verified: bun lint clean, bun run test:unit clean modulo the documented pre-existing 'Cannot find package #rpc' from client/rpc/rpc-client.ts.
This was referenced May 16, 2026
Proletter
pushed a commit
that referenced
this pull request
May 24, 2026
…questId (#2060) * QVAC-18183 feat[api]: non-inference migrations + decorated-promise requestId Migrate the last three pre-M3 long-running handlers onto the `RequestRegistry` and expose `requestId` synchronously on the Promise-shaped public-API calls. Deliverables (per the M3c implementation brief): 1. **`rag`** (kind `"rag"`) — `server/rpc/handlers/rag.ts` now opens `await using ctx = registry.begin({ kind: "rag" })` for every workspace-bound op (`ingest`, `saveEmbeddings`, `reindex`). `rag-operation-manager.ts` is rewritten around a small workspace → requestId map (`get/set/clearActiveRagRequest`) plus a `cancelAllRagOperations` shutdown sweep. The pre-existing `activeOperations` / `registerRagOperation` / `unregisterRagOperation` / `cancelRagOperation` API is removed — nothing outside the manager touched it. 2. **`downloadAsset`** (kind `"downloadAsset"`) — handler wraps the resolve flow in `registry.begin(...)` and threads `ctx.signal` plus a `DownloadHooks.requestBinding` into `resolveModelPath(...)` / `resolveModelPathWithStats(...)`. In `download-manager.ts`, `startOrJoinDownload` now accepts a per-subscriber `SubscriberRequestBinding` ({ signal, scope, requestId }); the content-addressed dedup keyed on `downloadKey` is preserved (two callers requesting the same artifact still share one transfer) but each subscriber attaches an abort listener that rejects its promise alone, and the new `maybeCancelTransfer(transfer)` helper aborts the underlying transfer only when the **last** subscriber leaves. `scope.defer(() => removeSubscriber(...))` is the safety-net cleanup for handler-exit paths that don't go through the abort listener. 3. **`loadModel`** (kind `"loadModel"`) — handler wraps the download-then-load flow in `registry.begin(...)`, threads `ctx.signal` into the resolver chain, and gates the `plugin.createModel(...)` / `model.load(false)` call on `ctx.signal.aborted` before and after. Load phase is soft-cancel only — the addon's load path doesn't accept a signal today; documented as a follow-up. 4. **Decorated-promise `requestId`** — new helper `packages/sdk/utils/decorate-promise.ts` (`Object.assign`-based, intentionally not a prototype extension). `loadModel(...)` and `downloadAsset(...)` return `Promise<string> & { requestId: string }` so callers can grab `op.requestId` synchronously and fire `cancel({ requestId: op.requestId })` before the network round-trip resolves. The unwrap chain is preserved: `await loadModel(...)` still returns the model id, pinned by `test/unit/utils/decorate-promise.test.ts`. 5. **Cursor rules** — `request-lifecycle-primitives.mdc` flips the migration table to mark M3b/M3c shipped, documents the decorated- promise pattern, and adds the "what's on the registry today" dispatch-level truth table. `docs/request-lifecycle-system.mdc` flips the M3c roadmap row. §4 Open decisions (resolved): - **Decision A — cancel error class:** A.1, reuse `InferenceCancelledError` (52419) across all non-inference handlers. Zero new public-API surface. The naming oddity is filed as a potential rename follow-up. - **Decision B — RAG workspace-level admission:** B.2, dispatcher- level pre-emption in `rag.ts`. Before `registry.begin({ kind: "rag" })`, the dispatcher walks the workspace→requestId map and calls `registry.cancel({ requestId: prev })` for any prior in-flight op on the same workspace. No new field on `ConcurrencyPolicy`. The cancel-prior → begin-new order is load-bearing — reversing it would cancel the just-installed context. - **`resolveModelPath` signal threading:** `signal: AbortSignal` parameter, not a full `ctx: RequestContext`. The resolver doesn't need the context — just the signal — so the narrower parameter is the idiomatic shape across all call sites (`http.ts`, `hyperdrive.ts`, `registry.ts`, `resolve-session.ts`). `cancelHandler.ts`'s `case "downloadAsset"` and `case "rag"` arms stay intact (M1 compat contract). They now emit a deprecation log line and route through `registry.cancel({ requestId })` rather than the legacy direct paths. M3d retires the dispatcher entirely. Tests added: - `test/unit/utils/decorate-promise.test.ts` — sync metadata access, unwrap-to-T, rejection propagation, in-place identity, chain integrity. - `test/unit/download-manager-subscriber.test.ts` — dedup preserved, per-subscriber cancel doesn't affect siblings, last-subscriber leaving aborts the transfer, `scope.defer` safety net, cancel error carries `requestId`. - `test/unit/rag-workspace-preempt.test.ts` — workspace map get/set/clear semantics + the "still mine?" guard against stale scope-unwind clears. - `test/unit/request-id-wire.test.ts` — `loadModel` / `downloadAsset` / `rag` request schemas preserve `requestId` on the wire and treat it as optional for legacy clients. `bun lint`, `bun run typecheck`, `bun test test/unit/` (additive change — pre-existing unrelated `tts-multicast` test failure on this worktree is from missing `dist/` for the `#rpc` package alias; not introduced here). * QVAC-18183 chore: address PR #2060 review nits - Soften M3b roadmap rows to "in review (#2058)" in request-lifecycle primitives + system docs until #2058 merges (avoids briefly advertising M3b as shipped while M3c lands first). - Promote the "no in-flight match" log in cancelHandler.ts's `case "request"` arm from debug to info: the decorated-promise pattern makes post-settle Stop-button cancels a common, user-visible code path and needs to surface without lowering the log level. - Route cancelTransfer's orphan-subscriber settle through removeSubscriber(...) so the last-subscriber teardown rule is enforced in one place (drops a redundant inline abort guard). * QVAC-18183 doc: trim internal milestone references from cursor rules (#2060 review feedback) Removed the "Migration Roadmap" table, "M1/M2/M3a-d" milestone labels, planning-brief decision references (Decision A/B.2, D1/D2), workspace-local paths (`tasks/release-0.11.0-planning/...`, `pitch-3-decisions.md`), and "in review #2058" forward-references from the request-lifecycle cursor rules and the matching code comments. The dispatch-level "What's on the registry today" truth table remains and now reflects only the kinds actually routed through the registry on this branch.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🎯 What problem does this PR solve?
loadModel,downloadAsset, andragwere the last three long-running server-side handlers without registry-routed cancellation. Cancelling them meant either reaching intocancelHandler.ts's legacycase "downloadAsset"/case "rag"arms (workspace-wide or download-key-wide blast radius) or waiting for shutdown — there was nocancel({ requestId })parity withcompletion/embeddings/transcribe/etc.activeTransfers(download-manager.ts) andactiveOperations(rag-operation-manager.ts). Two sources of truth for "what's running" meant cancel routing throughregistry.cancel(...)could disagree with the ad-hoc maps, especially during shutdown sweeps.loadModel(...),downloadAsset(...)) didn't exposerequestIdsynchronously, so a client UI couldn't wire a Stop button to a specific in-flight call before itsawaitresolved — onlycompletion(...)had that contract (via theCompletionRunobject).📝 How does it solve it?
rag(kind"rag") —server/rpc/handlers/rag.tsnow opensawait using ctx = registry.begin({ kind: "rag" })for every workspace-bound op (ingest,saveEmbeddings,reindex).rag-operation-manager.tsis rewritten around a small workspace → requestId map plus a shutdown sweep (cancelAllRagOperations). The oldactiveOperations/registerRagOperation/unregisterRagOperation/cancelRagOperationAPI is removed.downloadAsset(kind"downloadAsset") — handler wraps the resolve flow inregistry.begin(...)and threadsctx.signalplus aDownloadHooks.requestBindingintoresolveModelPath(...). Indownload-manager.ts,startOrJoinDownloadnow accepts a per-subscriber binding ({ signal, scope, requestId }). Content-addressed dedup keyed ondownloadKeyis preserved (two callers requesting the same artifact still share one transfer) but each subscriber attaches an abort listener that rejects its promise alone, and the newmaybeCancelTransfer(transfer)helper aborts the underlying transfer only when the last subscriber leaves.scope.defer(() => removeSubscriber(...))is the safety-net cleanup for handler-exit paths that don't go through the abort listener.loadModel(kind"loadModel") — handler wraps the download-then-load flow inregistry.begin(...), threadsctx.signalinto the resolver chain, and gates the addon-levelplugin.createModel(...)/model.load(false)call onctx.signal.abortedbefore and after. The load phase is soft-cancel only — the addon's load path doesn't accept a signal today; documented as a follow-up.loadModelopens its registry context withmodelId: undefined.LoadModelRequestfor a fresh load doesn't carry amodelIduntil the config-hash is computed inside the handler, so the registry slot is keyed byrequestIdalone. Implication:cancel({ modelId, kind: "loadModel" })is a no-op for in-progress loads — cancel byrequestId(viaop.requestIdon the decorated promise) is the only path. This is captured in the inline comment on the handler and the "what's on the registry today" dispatch-level table inrequest-lifecycle-primitives.mdc; flagging here so reviewers don't have to re-derive it.requestId— new helperpackages/sdk/utils/decorate-promise.ts(Object.assign-based, intentionally not a prototype extension).loadModel(...)anddownloadAsset(...)returnPromise<string> & { requestId: string }so callers can grabop.requestIdsynchronously and firecancel({ requestId: op.requestId })before the network round-trip resolves. The unwrap chain is preserved:await loadModel(...)still returns the model id (backward-compat).request-lifecycle-primitives.mdcflips the migration table to mark M3b/M3c shipped, documents the decorated-promise pattern, and adds the "what's on the registry today" dispatch-level truth table.docs/request-lifecycle-system.mdcflips the M3c roadmap row.§4 Open decisions (resolved)
InferenceCancelledError(52419) across non-inference handlers. Zero new public-API surface. The naming oddity is filed as a potential rename follow-up.rag.ts. Beforeregistry.begin({ kind: "rag" }), the dispatcher walks the workspace→requestId map and callsregistry.cancel({ requestId: prev })for any prior in-flight op on the same workspace. No new field onConcurrencyPolicy. The cancel-prior → begin-new order is load-bearing.resolveModelPathsignal threading:signal: AbortSignalparameter, not a fullctx: RequestContext. The resolver doesn't need the context — just the signal — so the narrower parameter is the idiomatic shape across all call sites.cancelHandler.ts'scase "downloadAsset"andcase "rag"arms stay intact (M1 compat contract). They now emit a deprecation log line and route throughregistry.cancel(...)rather than the legacy direct paths. M3d retires the dispatcher entirely.🧪 How was it tested?
bun lint— cleanbun run typecheck— cleanbun test test/unit/— all new tests green; pre-existing unrelatedtts-multicast.test.tsfailure on this worktree is aCannot find package '#rpc'from missingdist/, present on a clean checkout of the base branch too (not introduced here)New tests:
test/unit/utils/decorate-promise.test.ts— sync metadata access, unwrap-to-T(the backward-compat pin:await loadModel(...) → string), rejection propagation, in-place identity,.then/.catch/.finallychain integrity, multi-field decoration.test/unit/download-manager-subscriber.test.ts— dedup preserved (two callers share one transfer), per-subscriber cancel doesn't affect siblings, last-subscriber leaving aborts the transfer,scope.defersafety net for handler-exit paths, cancel error carriesrequestId.test/unit/rag-workspace-preempt.test.ts— workspace map get/set/clear semantics + the "still mine?" guard against stale scope-unwind clears stomping a newer op's mapping.test/unit/request-id-wire.test.ts—loadModel/downloadAsset/ragrequest schemas preserverequestIdon the wire and treat it as optional for legacy clients (server falls back to a server-generated id).Known follow-up
loadModelload phase is soft-cancel: a cancel that arrives duringplugin.createModel(...)/model.load(false)lands on the registry but the addon runs to completion. The handler still rejects the client's promise withInferenceCancelledError, but the model is now registered as loaded (orphan-model trade-off). A per-load cancel surface on the addon would close this; tracked as a separate follow-up.🔌 API Changes
Decorated-promise return shape for
loadModel/downloadAssetBefore:
After:
Same shape for
downloadAsset:The return type is
Promise<string> & { requestId: string }. Existing call sites that useconst id = await sdk.loadModel(...)keep working without modification — the decoration adds a property without touching the prototype chain.Per-
requestIdcancel forragThe
raghandler now emits arequestIdin the registry for every workspace-bound op (ingest/saveEmbeddings/reindex). Clients targeting a specific RAG op via the wire envelope can cancel it directly:The legacy
sdk.cancel({ operation: "rag", workspace })arm still works (now routed through the registry with a deprecation log line; removed in M3d).