Phase 26: Twelve Labs Async Embedding#509
Conversation
Research F-01 collapsed the task API to two endpoints (POST /tasks and
GET /tasks/{id}); the in-scope bullet still listed a third /status
sub-path that does not exist. Aligns CONTEXT with RESEARCH and with the
plans to prevent executor drift.
…ayload
Adds a `FailureDetail json.RawMessage` field to TaskResponse (json:"-")
and has doTaskGet populate it with the raw response bytes on each GET
/tasks/{id}. This lets Plan 02's failed-status branch sanitize the
authentic server-provided reason instead of re-marshaling the parsed
struct subset, which was dropping undeclared reason fields (codex HIGH,
D-17 compliance).
…used rejection - Per-HTTP-call deadline (codex HIGH): every doTaskGet is now wrapped in context.WithDeadline(ctx, min(parentDL, sdkMaxWaitDeadline)) so a blocked/hung HTTP request cannot outlive maxWait (D-09 hard bound). Translates deadline-source back into distinct errors — SDK maxWait vs parent ctx.DeadlineExceeded (D-20). - Fused-on-async rejection (codex/gemini HIGH): contentToAsyncRequest now rejects audioOpt == "fused" with a deterministic validation error; async endpoint accepts only "audio" and "transcription" (RESEARCH F-02 / A5). - Failed-reason authenticity (codex HIGH): failed-status branch uses resp.FailureDetail (raw server body from Plan 01) rather than re-marshaling the parsed TaskResponse (D-17). - Distinct ctx-cancel vs deadline wording (codex MEDIUM): context.Canceled wraps as "async polling canceled"; context.DeadlineExceeded wraps as "async polling deadline exceeded".
… malformed - Use the existing embeddings.ConfigInt helper (pkg/embeddings/embedding.go:674) instead of reimplementing int/int64/float64 switching (codex LOW). - Distinguish missing key from malformed value: async_polling=true AND malformed async_max_wait_ms no longer silently falls back to WithAsyncPolling(0) (which would enable async with the 30-minute default on broken config). The flag stays off rather than enabling on a broken round-trip (codex MEDIUM).
…tests
- Fix compile blocker: replace all 5 embeddings.ContentPart references
with embeddings.Part (codex HIGH/compile-blocker — multimodal.go:61
defines Part, there is no ContentPart in the embeddings package).
- Add TestTwelveLabsAsyncFailedReasonSanitized: long-reason failed-task
fixture asserts the authentic server reason reaches the error and is
sanitized (ratchets D-17 via a runtime test).
- Add TestTwelveLabsAsyncBlockedHTTPMaxWait: blocking httptest handler
proves the per-HTTP-call deadline added in Plan 02 actually unblocks
an in-flight GET when maxWait fires.
- Add TestTwelveLabsAsyncFusedRejected: WithAudioEmbeddingOption("fused")
+ WithAsyncPolling + audio content must return a deterministic
validation error before any HTTP call.
- Assert APIKeyEnvVar survives the config round-trip (gemini review).
- Add AsyncEmbedV2Request with AsyncAudioInput (embedding_option as []string) and AsyncVideoInput - Add TaskCreateResponse and TaskResponse with Mongo-style `_id` JSON alias - TaskResponse.FailureDetail (json.RawMessage, excluded from JSON) preserves raw body for D-17 sanitization - Extend TwelveLabsClient with unexported async polling fields - applyDefaults sets initial=2s, multiplier=1.5, cap=60s per D-11
- doTaskPost: POST {BaseAPI}/tasks with JSON body; mirrors doPost headers and error sanitization
- doTaskGet: GET {BaseAPI}/tasks/{id} with URL-escaped task ID; empty-ID guard rejects the Mongo-alias footgun
- Both use chttp.ReadLimitedBody + chttp.SanitizeErrorBody (Phase 25 convention)
- doTaskGet preserves raw response bytes in TaskResponse.FailureDetail (D-17) so Plan 02 can sanitize the authentic server failure reason
- Lint-silenced with //nolint:unused until Plans 26-02/03 wire the polling loop and option
- expose single public async trigger (D-03, D-04) - maxWait=0 selects 30-minute default - reject negative maxWait with explicit error - sets asyncPollingEnabled=true and asyncMaxWait on client
- GetConfig emits async_polling and async_max_wait_ms only when enabled (D-21, D-22) - FromConfig reads async_polling (bool) and async_max_wait_ms via embeddings.ConfigInt - malformed async_max_wait_ms leaves async disabled (no silent 30-min fallback) - missing keys = async off (opt-in only via WithAsyncPolling, D-23)
- pollTask: capped exponential backoff polling loop using time.NewTimer (no time.After leak per Pitfall 2) with per-HTTP-call deadline bounded by min(parent ctx deadline, SDK maxWait deadline) so a blocked doTaskGet cannot outlive maxWait (D-09 hard bound). - Distinct error wording for ctx.Canceled, ctx.DeadlineExceeded, and SDK-side maxWait expiry (D-20). errors.Is still unwraps to stdlib sentinels via pkg/errors. - Terminal status=failed uses raw server body from TaskResponse.FailureDetail through chttp.SanitizeErrorBody — no re-marshaled struct subset (D-17). - D-16 default branch rejects unknown status values. - contentToAsyncRequest rejects audio embedding option 'fused' on the async path per RESEARCH F-02 / Assumption A5 (async accepts only 'audio' and 'transcription'). - createTaskAndPoll orchestrates build -> POST -> optional early-ready short-circuit -> poll -> extract embedding. - //nolint:unused annotations on the symbols until Task 2 wires routing in content.go (pattern from Plan 26-01).
- document WithAsyncPolling public surface and round-trip keys - note malformed-value → async-off decision (no silent 30-min fallback) - record reuse of embeddings.ConfigInt helper across numeric JSON types
…abled - embedContent now dispatches to createTaskAndPoll when asyncPollingEnabled is true AND the single part is audio or video (CONTEXT.md D-07). Text/image always take the sync path (D-07). - When asyncPollingEnabled is false, all four modalities continue on the sync doPost path — zero behavior change for non-opt-in callers (D-08). - Removed //nolint:unused annotations from twelvelabs_async.go — all symbols are now reachable from embedContent via the routing switch.
…ed-status tests - Add newTestAsyncEF helper with ms-scale polling intervals - Add audioContent/videoContent + taskCreateJSON/taskGetJSON fixtures - Fixtures emit _id (not id) to guard Pitfall 1 alias gotcha - Cover 4 of 6 D-26 flows: create, poll-to-ready, poll-to-failed, unexpected-status
…rip, and review-fix tests - TestTwelveLabsAsyncCtxCancel: proves errors.Is(err, context.Canceled) (D-10/D-19) - TestTwelveLabsAsyncMaxWait: proves 'async polling maxWait' distinct from DeadlineExceeded (D-20) - TestTwelveLabsAsyncSkipsTextImage: text/image never hit /tasks even when async is on (D-07) - TestTwelveLabsAsyncConfigRoundTrip: async_polling/async_max_wait_ms + APIKeyEnvVar survive round-trip (D-23) - TestTwelveLabsAsyncConfigOmitWhenDisabled: async keys omitted when disabled (D-22) - TestTwelveLabsAsyncFailedReasonSanitized: raw server reason survives sanitization (D-17) - TestTwelveLabsAsyncBlockedHTTPMaxWait: per-call deadline unblocks hanging GET within maxWait - TestTwelveLabsAsyncFusedRejected: fused+async rejected before POST /tasks (F-02)
Code Review: Phase 26 — Twelve Labs Async EmbeddingBug:
|
… transport errors - .github/workflows/go.yml: pass GITHUB_TOKEN to both crosslang test steps so pure-onnx's bootstrap can use authenticated GitHub API requests. The cross-language tests auto-wire the default EF from server collection config, which triggers ONNX runtime bootstrap; anonymous GitHub API quota (60 req/hr per runner IP) was causing intermittent failures surfacing as "embedding function is required" when bootstrap failed silently during auto-wire. - twelvelabs_async.go: mirror the context.DeadlineExceeded branch and wrap the original transport error (err) in the context.Canceled branches of pollTask and createTaskAndPoll. Previous code wrapped ctx.Err() which both discarded the transport chain and risked (nil, nil) returns from pkg/errors.Wrap(nil, ...), violating the library's no-panic contract.
PR Review: Phase 26 — Twelve Labs Async EmbeddingOverall this is well-structured with thorough test coverage and careful deadline/cancellation handling. A few issues found: Bug:
|
- createTaskAndPoll: treat status=failed on the POST /tasks response as
a terminal error with a distinct message, and treat status=ready as
terminal regardless of data shape (let buildEmbeddingFromData surface
malformed responses). Avoids a wasteful GET round-trip in both cases.
- buildMediaSource: reject URL sources whose scheme is not http or
https. Defense-in-depth against sending non-web schemes to the
Twelve Labs API via the client.
- Add tests:
- TestTwelveLabsAsyncCreateReturnsFailed pins the no-GET path for
failed-on-create.
- TestTwelveLabsAsyncCreateReturnsReadyEmptyData pins the no-GET
path for malformed ready-with-no-data.
- TestBuildMediaSourceURLValidation extends with ftp/gopher/file
scheme rejection and plain http acceptance.
Code Review: Phase 26 — Twelve Labs Async EmbeddingOverall this is a well-structured implementation. The polling loop, deadline propagation, and error classification are thorough. A few findings below. Bugs / Issues1. No minimum validation on
2. SDK maxWait timeout errors are not programmatically distinguishable (
3. Stale PR review note The PR body states: "Initial doTaskPost is not yet bounded by asyncMaxWait". But the code does bound it -- Minor / Informational4. Empty failure detail produces slightly odd error message If a server returns 5. Config round-trip test does not cover JSON serialization
What looks good
No critical bugs or security issues found. |
- Reject sub-floor asyncMaxWait at option-application time (< 2s can't complete one poll cycle); extract defaultAsyncPollInitial constant so applyDefaults and WithAsyncPolling share a single floor source of truth. - Introduce ErrAsyncMaxWaitExceeded sentinel wrapped via fmt.Errorf %w at all 4 maxWait-exceeded sites so callers can detect SDK-enforced timeouts via errors.Is without string matching. Chain-break from context.DeadlineExceeded preserved (D-20). - sanitizeTaskFailureDetail returns "(no failure detail provided)" when extractTaskFailureDetail yields nothing and the body is a JSON object with no diagnostic fields, avoiding leakage of housekeeping fields (_id, status) into user-facing errors. - Extend TestTwelveLabsAsyncConfigRoundTrip with json.Marshal/Unmarshal so the float64-coercion path used by registry-persisted configs is locked in.
PR Review — Phase 26: Twelve Labs Async EmbeddingOverall this is a well-structured PR with solid timeout/cancellation handling and good test coverage. Two issues found: Bug: Task-creation failure discards server diagnostic detail
case taskStatusFailed:
return nil, errors.Errorf("Twelve Labs async task [%s] terminal status=failed at creation", created.ID)Compare with the polling path ( return nil, errors.Errorf("Twelve Labs task [%s] terminal status=failed: %s", taskID, sanitizeTaskFailureDetail(resp.FailureDetail))The Suggested fix: Either add raw-body capture to Latent bug:
|
…ime failure reason
- Add FailureDetail field to TaskCreateResponse and populate it in doTaskPost
when a 2xx response carries status=failed. The async.go error path now
surfaces the sanitized server reason via sanitizeTaskFailureDetail, matching
the GET /tasks symmetry (D-17) so rare create-time terminal failures are
debuggable instead of returning just the task ID.
- Reject WithAudioEmbeddingOption("fused") + WithAsyncPolling(...) in
validate() at NewTwelveLabsEmbeddingFunction time. The async /tasks endpoint
only accepts "audio" and "transcription" (RESEARCH F-02); previously this
combination constructed successfully and failed at first EmbedContent —
now it fails loudly at construction. Runtime defense-in-depth check in
contentToAsyncRequest is retained.
Code Review: Phase 26 — Twelve Labs Async EmbeddingOverall this is a well-structured addition with good test coverage (24+ async tests), proper context propagation, and careful error differentiation. A few issues worth addressing: Bug: Inconsistent HTTP status code acceptance between sync and async paths
if resp.StatusCode != http.StatusOK {While if resp.StatusCode < 200 || resp.StatusCode >= 300 {If the Twelve Labs API ever returns Bug: Default HTTP client has no timeout
c.Client = &http.Client{Timeout: 30 * time.Second}Potential issue:
|
Summary
Phase 26: Twelve Labs Async Embedding
Goal: Twelve Labs provider handles async task responses for long-running audio and video embeddings
Status: Verified on 2026-04-14 (
26-VERIFICATION.mdstatus:passed)This phase adds an explicit async path for Twelve Labs long-running media embeddings. Audio and video callers can now opt in with
WithAsyncPolling(maxWait)to create a task, poll until it reaches a terminal state, and return the final embedding or a sanitized failure. Text/image callers and all non-opt-in traffic remain on the existing synchronous path.Changes
Plan 26-01: Async HTTP foundation
Added dedicated async request/response types for the tasks API, decoded the
_idalias correctly, preserved raw task failure bodies for later sanitization, and introduced internal polling configuration fields/defaults plus task POST/GET helpers.Key files:
pkg/embeddings/twelvelabs/twelvelabs.goPlan 26-02: Polling loop and modality routing
Implemented the async polling loop with capped backoff, distinct cancellation/deadline/maxWait behavior, failure-body sanitization, fused-audio rejection on the async path, and audio/video routing into the tasks flow while leaving text/image unchanged.
Key files:
pkg/embeddings/twelvelabs/twelvelabs_async.gopkg/embeddings/twelvelabs/content.goPlan 26-03: Public option and config round-trip
Added the public
WithAsyncPolling(maxWait)option and made async config persist/rebuild throughasync_pollingandasync_max_wait_mswithout changing default behavior for callers who do not opt in.Key files:
pkg/embeddings/twelvelabs/option.gopkg/embeddings/twelvelabs/twelvelabs.goPlan 26-04: Async test coverage
Added 12 async-focused tests covering task creation, ready/failed/unexpected task states, context cancellation, SDK maxWait behavior, sync-path preservation for text/image, config round-trip, raw failure sanitization, blocked HTTP maxWait enforcement, and fused-mode rejection.
Key files:
pkg/embeddings/twelvelabs/twelvelabs_test.goRequirements Addressed
TLA-01: Twelve Labs provider detects async task responses and enters a polling loopTLA-02: Async polling respects caller context for cancellation and timeoutTLA-03: Async polling handles terminal states with appropriate result delivery or error messagesTLA-04: Tests cover async task creation, polling, completion, failure, and context cancellationVerification
26-VERIFICATION.md, 4/4 must-haves verified on 2026-04-14)go test -tags=ef -count=1 ./pkg/embeddings/twelvelabs/...make lintKey Decisions
WithAsyncPolling(maxWait); polling internals remain hidden./embed-v2.embedding_optionas[]string, and task responses decode Twelve Labs’_idfield.fusedaudio is rejected on the async path instead of being silently remapped.