Skip to content

v0.6.92: enrichment table column type, table run fixes, scheduled jitter, hosted-key queueing#4756

Merged
TheodoreSpeaks merged 5 commits into
mainfrom
staging
May 27, 2026
Merged

v0.6.92: enrichment table column type, table run fixes, scheduled jitter, hosted-key queueing#4756
TheodoreSpeaks merged 5 commits into
mainfrom
staging

Conversation

@TheodoreSpeaks
Copy link
Copy Markdown
Collaborator

TheodoreSpeaks and others added 5 commits May 26, 2026 19:52
…4416)

* Add queueing for hosted keys

* feat(rate-limiter): FIFO queue for hosted-key per-workspace fairness

Replace the per-call distributed lock with a Redis-backed FIFO queue so
callers within a workspace get strict ordering instead of racing the
bucket. Adds heartbeat-based crash recovery and dead-head reaping in a
single Lua script. Bumps Exa search hosted RPM from 5 to 60.

* fix(rate-limiter): bound hosted-key queue wait to execution budget; fix heartbeat + telemetry

Tie the per-workspace hosted-key queue wait to the surrounding execution
budget instead of a flat 5-minute cap. acquireKey now accepts the execution
AbortSignal (threaded from ExecutionContext): when present, the wait is
bounded by the run's actual plan timeout / cancellation, with the enterprise
async ceiling as a backstop; when absent it falls back to MAX_QUEUE_WAIT_MS.
This lets long-running async (Trigger.dev) runs use their full budget while
no longer letting a single queued call burn a short sync run's entire budget.

Also addresses Greptile review:
- P1: share one lastHeartbeatAt across all wait phases and cap every sleep to
  HEARTBEAT_REFRESH_INTERVAL_MS so a long low-RPM retryAfterMs can no longer
  let the head's heartbeat lapse mid-wait and break FIFO ordering.
- P2: derive hostedKeyQueueWaited telemetry reason from the actual bottleneck
  (queue_position / dimension / actor_requests) instead of hardcoding it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(rate-limiter): make hosted-key queue waits abort-interruptible

Replace the plain capped sleeps in the queue-head and bucket-capacity wait
loops with an interruptibleSleep that resolves early when the execution
AbortSignal fires (timeout or cancellation), cleaning up its own timer and
listener. Previously a cancelled/timed-out run could overshoot by up to the
heartbeat cap (~10s) before the loop re-checked its budget; now it wakes
within a tick. The cap remains for heartbeat renewal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…4750)

Cron schedules all fire on the same boundary (e.g. every :00), stampeding
the Postgres connection pool at the top of each minute/hour. Spread each due
schedule's start across a [0, 30s) window via trigger.dev's delay option
(no compute billed during the delay). Wires the previously-unused
EnqueueOptions.delayMs through the trigger.dev backend.
* feat(tables): native enrichments sidebar + workflow input mapping

Add a Clay-style enrichments catalog to the table view and wire per-row
input mapping into workflow-backed columns.

- New "Enrichments" entry in the New-column dropdown opens a sliding panel
  listing curated enrichment templates; picking one swaps to the workflow
  config in-place (no cross-slide) with a back button.
- Type the workflow sidebar as manual | enrichment; enrichment hides the
  launch + add-column-inputs affordances.
- Add a "Workflow inputs" advanced panel mapping Start-block input fields to
  table columns (left-of-workflow columns only), with name-match auto-fill
  and collapsible input-mapping-style rows.
- Persist type + inputMappings on the workflow group (types, contract, route,
  service, hook) — jsonb, no migration.
- Consume inputMappings at run time: when present, feed Start-block fields
  from the mapped columns; otherwise fall back to name-match spread.
- Clean up inputMappings on column rename/delete (stripGroupDeps + renameColumn).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(emcn): extract CollapsibleCard and reuse for input mapping

Pull the collapsible field-card markup (surface-4 header + surface-2 body,
click/keyboard toggle, truncated title + optional badge) into a shared
`CollapsibleCard` emcn component, and use it in the workflow-builder input
mapping rows and the table sidebar's input-mapping panel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(tables): code-defined enrichment registry run directly per row

Enrichments are now TS configs in apps/sim/enrichments/ (registry, like
connectors) that run directly per table row via the existing run/dispatch/
cell-write rails — no workflow execution.

- enrichments/{types,registry} + work-email (heuristic) and phone-number (stub).
- WorkflowGroup gains enrichmentId; WorkflowGroupOutput gains outputId
  (workflowId/blockId/path kept required, '' for enrichment groups).
- Executor branches on group.type === 'enrichment' → maps inputMappings →
  enrich() → outputs by outputId → cell-write. Missing required inputs skip
  (blank cell) instead of erroring.
- Sidebar lists the registry; enrichment-config panel maps inputs to columns
  and creates the enrichment group (no workflow UI).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(enrichments): provider fallback cascade + hosted-key usage source

Replace each enrichment's single enrich() with an ordered providers[]
fallback cascade. Providers are plain data ({ id, label, toolId,
buildParams, mapOutput }) so the catalog stays client-safe; the
server-only runner (run.ts) calls executeTool per provider, first
non-empty result wins, misses/errors fall through, all-miss = blank cell.

Wire four enrichments on the hosted-safe providers (Hunter, PDL):
- Work Email (fullName, companyDomain): Hunter -> PDL
- Phone Number (fullName, companyDomain): PDL
- Company Domain (companyName): PDL
- Company Info (domain): PDL -> Hunter

Person enrichments take a single canonical fullName (Clay-style); Hunter
gets first/last via splitName(), PDL takes name directly.

Add 'enrichment' to usage_log_source enum (+ migration) so hosted-key
tool cost from these per-row calls can be billed to the table owner.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(enrichments): bill hosted-key cost; surface provider errors; abort safety

- runEnrichment now returns { result, cost, error }: accumulates hosted-key
  cost across the cascade, and sets `error` only when every provider that ran
  errored (auth/rate-limit/outage) vs a clean miss.
- Executor records the cost to the table owner (createdBy) via recordUsage
  (source 'enrichment'); billing failures are logged, never error the cell.
- F1: all-providers-errored now writes status 'error' instead of a blank
  'completed' cell that looked like "no data found".
- F2: re-check the abort signal after the cascade so a cancel mid-tool-call
  isn't recorded as a completed empty cell.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(tables): present enrichment columns as first-class in the grid

- Meta-header shows the enrichment's name + icon (Mail/Phone/Globe/Building2)
  instead of "Workflow" + a color chip.
- Per-column header icon uses the enrichment's icon (via columnSourceInfo)
  instead of the generic play icon.
- Hide "View execution" for enrichment cells in both the row context menu and
  the action bar (no workflow execution exists to open); also hide the
  meta-menu "View workflow" item for enrichment groups.
- Clicking an enrichment column header now opens the enrichments sidebar in
  edit mode (pre-filled input mappings, Update via useUpdateWorkflowGroup)
  instead of the workflow "Configure workflow" sidebar.
- Enrichment config lets the user name each output column (editable per-output,
  deduped defaults) since enrichments can produce multiple columns.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(tables): enrichment columns use type icon; output names editable

- Drop the per-column enrichment icon (it duplicated the meta-header icon).
  Enrichment output columns now render the standard column-type icon (Text,
  etc.) — the enrichment's icon stays only on the group meta-header.
- Make output column names editable in the enrichment config edit mode too;
  changed names rename their columns via useUpdateColumn (the rename cascades
  into the group's output refs server-side). Validation excludes the output's
  own current name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(tables): wrap enrichment catalog descriptions instead of truncating

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(tables): edit enrichment output columns via the plain column editor

Edit column on an enrichment output now opens the normal column-config sidebar
(rename / type / unique) instead of the workflow 'Configure output column'
panel, which showed workflow-only fields and blocked a simple rename.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(copilot): list_enrichments + add_enrichment table tools

Let the copilot enumerate the code-defined enrichment registry and add an
enrichment column to a table (validating required input mappings against the
table's columns), backed by the same workflow-group machinery the UI uses.

* fix(enrichments): address PR review feedback

- Guard the enrichment cell path on `enrichmentId` so a group typed
  'enrichment' without a registry id falls through to the workflow path
  instead of erroring.
- Clear stale output values when skipping a row for missing required inputs,
  so the auto cascade re-enriches once inputs return (was left completed+filled).
- Write a terminal state on abort in the enrichment path (matches the workflow
  path) so a cancel between run and terminal-write can't leave the cell running.
- Edit mode: apply the group update (mappings/deps/auto-run) before column
  renames so the primary edit lands even if a rename fails.
- Disable Save once validation has surfaced a missing required input.
- Use the workflowGroupById map instead of O(n) find in the context-menu and
  action-bar hot paths.

* chore(commands): add /add-enrichment command

Guides adding a code-defined table enrichment to the registry, with a required
step to verify each provider tool has hosted-key support and chain to
/add-hosted-key when it doesn't.

* fix(enrichments): address second-pass PR review

- updateWorkflowGroup output diff now keys on outputId (falling back to
  blockId::path) so enrichment outputs — which share empty blockId/path —
  no longer collapse to one key and drop sibling columns.
- Enrichment terminal write now clears output columns absent from the result,
  so a partial/empty re-run doesn't leave stale values.
- Editing a group whose enrichment was removed from the registry shows an
  explanatory panel instead of silently falling through to the new-enrichment
  catalog.

* feat(tables): show "Not found" badge for empty completed enrichment cells

An enrichment that runs to completion but matches nothing now renders a gray
"Not found" badge (like the Queued/Waiting cell states) instead of a blank
cell, so a real miss is distinguishable from an unrun cell. Scoped to
enrichment output columns; an empty string no longer counts as a value.

* fix(enrichments): don't re-run completed no-match enrichments on auto cascade

A completed enrichment with empty outputs is a real no-match result, not an
unfinished run. Eligibility now treats an enrichment's completed status as
terminal (regardless of output fill), so the auto cascade stops re-invoking
billable provider calls on every no-match row each dispatch. Input changes
still clear the exec entry, so genuine re-runs are unaffected; manual Run all
still re-runs.

* fix(enrichments): treat provider 404 as no-match, not a cell error

Providers like People Data Labs signal 'no record found' with HTTP 404, which
executeTool surfaces as a failed ToolResponse (status on output.status). The
cascade now treats a 404 as a clean miss — falls through to the next provider
and lets the cell render 'Not found' — instead of marking the cell errored.
Auth/rate-limit/5xx still propagate as real errors.

* fix(tools): surface HTTP status on error ToolResponse output

executeTool's catch handled Error instances in its first branch and only
extracted status/statusText/data for non-Error object throws — so HTTP errors
(thrown as Error instances carrying .status) lost their status on the returned
output. Surface it for Error instances too, so callers can branch on the
status (e.g. the enrichment cascade treating a provider 404 as a no-match).

* fix lint

* Revert ff

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(db): disable statement_timeout for migrations

* fix(ci): route migration workflow through guarded migrate.ts

* feat(tables): workflow-column run fixes + bounded "run N rows"

- Pass group.autoRun as the add-group dispatch flag so an autoRun=false
  column no longer opens a no-op dispatch that flashes the run-count badge.
- Scope the context-menu re-run to the right-clicked workflow cell's group
  (cascading to dependents) instead of every group on the row.
- Add an extensible per-dispatch row cap (DispatchLimit { type:'rows', max })
  surfaced as "Run 10 / 1,000 empty rows" in the group header; dispatcher
  stops after N eligible rows. New limit/processed_count columns on
  table_run_dispatches.
- Fix stranded "Queued" cells: the cascade owner now treats a queued marker
  (orphan pre-stamp) as a manual run so autoRun=false requested groups are
  picked up, and drains late markers before releasing the row lock.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(db): regenerate dispatch limit migration on staging chain (0214)

Re-numbers the table_run_dispatches limit/processed_count columns from the
collided 0212 to 0214 after merging staging (which added its own 0212/0213).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(tables): lint formatting

* fix(tables): address PR review on dispatch cap + cascade drain

- Don't consume the row cap when batchEnqueueAndWait fails; a transient
  failure no longer completes a capped dispatch with zero rows started.
- Outer cascade-drain loop only re-drives a genuine queued marker, not any
  eligible group, so an empty-output group can't re-run forever.
- completeDispatch forwards limit on the terminal SSE event.
- Extract shared LIMITED_RUN_PRESETS for the Run-N-rows menu items.

* chore(lint): format generated tool-schemas-v1

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 27, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment May 27, 2026 9:29am

Request Review

@cursor
Copy link
Copy Markdown

cursor Bot commented May 27, 2026

PR Summary

High Risk
Touches core table execution (cascade locking, dispatch limits, enrichment billing via hosted keys) and new paid data paths; regressions could mis-run rows, over/under-bill, or leave stale cell state.

Overview
Adds Clay-style table enrichments: a registry-driven catalog (work-email, phone-number, company-domain, company-info) that creates enrichment workflow groups, maps inputs to columns, and runs a hosted-tool provider cascade per row (with usage billing) instead of a workflow. The UI gets an Enrichments slideout, + New column → Enrichments, group meta headers with enrichment icons, and completed empty hits shown as Not found.

Workflow / run behavior is tightened: optional inputMappings from Start-block fields to table columns, cell-scoped context-menu run/re-run for a single output group, Run N empty rows caps (RunLimit) through API/SSE/dispatcher, and a post-cascade loop that only re-drives queued markers after lock release. Enrichment and workflow fixes include skipping optimistic stamps for capped runs, ignoring capped dispatches in the ahead-of-cursor overlay, and syncing autoRun when adding groups so staged groups don’t flash a no-op dispatch.

Ops / polish: scheduled executions get 0–30s random enqueue jitter; Copilot gains list_enrichments / add_enrichment; shared CollapsibleCard and an internal /add-enrichment command doc the pattern.

Reviewed by Cursor Bugbot for commit 92fd17c. Bugbot is set up for automated code reviews on this repo. Configure here.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 92fd17c. Configure here.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 27, 2026

Greptile Summary

This PR bundles five distinct changes: a new enrichment table column type (registry-backed provider cascade with billing), workflow-column run fixes (bounded dispatches, inputMappings rename cascade, enrichment isDone semantics), a 0–30 s schedule execution jitter, Redis-backed FIFO queuing for hosted-key 429 recovery, and a DB failure-cause log helper.

  • Enrichment column type: a code-defined enrichment catalog runs a provider cascade per row, writes output columns, and bills hosted-key cost via recordUsage; the isDone enrichment guard prevents billable re-runs on no-match rows by treating completed (even with empty outputs) as terminal.
  • Hosted-key queuing: HostedKeyQueue (Redis list + per-ticket heartbeat) wraps acquireKey in a FIFO wait loop with heartbeat-aware sleeping and AbortSignal-bounded timeouts, replacing the previous immediate 429.
  • Dispatcher bounded runs: DispatchLimit + processedCount on tableRunDispatches supports "run first N rows" dispatch; budget resets to zero on batch-enqueue failure to avoid over-counting transient errors.

Confidence Score: 4/5

Safe to merge; all three findings are edge cases unlikely to trigger in normal usage and do not affect correctness of the happy path.

The queue list TTL is only refreshed on new enqueues, not during the active head waiter's heartbeat cycle. Under a very long wait with no new callers the list can expire, causing all waiters to bypass FIFO ordering simultaneously. The interruptibleSleep TOCTOU window is real but bounded to one extra sleep period by design. The stale enrichmentId field carried across outer-loop iterations is currently harmless but could mislead future code that inspects the payload.

apps/sim/lib/core/rate-limiter/hosted-key/queue.ts (queue list TTL refresh) and apps/sim/background/workflow-column-execution.ts (stale enrichmentId in re-drive payload)

Important Files Changed

Filename Overview
apps/sim/lib/core/rate-limiter/hosted-key/queue.ts New Redis-backed FIFO queue for hosted-key acquisitions; Lua EVAL script atomically reaps dead heads. Queue list TTL (600 s) is only refreshed on enqueue, not during active waiting, which can collapse ordering under long waits with no new callers.
apps/sim/lib/core/rate-limiter/hosted-key/hosted-key-rate-limiter.ts Replaces immediate 429 returns with a FIFO-queued wait loop backed by the new HostedKeyQueue; heartbeat-aware sleeping, interruptible sleeps, and telemetry events added. Minor TOCTOU window exists in interruptibleSleep between AbortSignal check and addEventListener.
apps/sim/background/workflow-column-execution.ts Adds outer re-drive loop for queued markers after cascade lock release; new enrichment-group fast path calls registry + runEnrichment instead of a full workflow. Stale enrichmentId can carry forward in currentPayload across iterations but is harmless under current routing logic.
apps/sim/lib/table/dispatcher.ts Adds DispatchLimit/processedCount support for bounded-row runs; row-cap accounting resets correctly on batch-enqueue failure to prevent budget exhaustion from transient errors.
apps/sim/app/api/schedules/execute/route.ts Adds 0-30 s random jitter to scheduled execution enqueues; concurrencyKey prevents duplicate runs so the jitter does not introduce double-execution.
apps/sim/enrichments/run.ts Clean provider cascade runner: 404 treated as no-match, other failures fall through, hosted-key cost accumulated across providers.
apps/sim/lib/table/workflow-columns.ts Enrichment isDone logic correctly prevents billable no-match re-runs; pickNextEligibleGroupForRow now correctly propagates isManualRun flag from queued markers.
packages/db/schema.ts Adds limit (JSONB) and processedCount columns to tableRunDispatches; new enrichment value added to usageLogSourceEnum. Migration files present.
apps/sim/lib/workflows/executor/execution-core.ts Adds describeErrorCause to walk Drizzle/driver error chains and surface PostgreSQL diagnostics alongside the top-level execution failure log.

Sequence Diagram

sequenceDiagram
    participant Caller as Tool Caller
    participant RL as HostedKeyRateLimiter
    participant Q as HostedKeyQueue (Redis)
    participant Bucket as Token Bucket

    Caller->>RL: acquireKey(provider, prefix, config, workspaceId, signal)
    RL->>Q: enqueue(provider, workspaceId, ticketId)
    Q-->>RL: position and enabled

    loop Poll until head
        RL->>Q: checkHead(provider, workspaceId, ticketId)
        Note over Q: Lua EVAL reap dead head check position
        Q-->>RL: waiting or head or missing
        RL->>Q: refreshHeartbeat every 10s
    end

    RL->>Bucket: waitForActorCapacity loop until token available
    Bucket-->>RL: capacity granted

    RL-->>Caller: success true key

    Note over RL,Q: finally dequeue ticketId
    RL->>Q: dequeue(provider, workspaceId, ticketId)
Loading

Reviews (1): Last reviewed commit: "fix(tables): workflow-column run fixes +..." | Re-trigger Greptile

Comment on lines +17 to +21
/**
* TTL on the queue list itself. Set on every enqueue. Prevents abandoned queues
* (whole workspace went silent) from sticking around forever in Redis.
*/
const QUEUE_LIST_TTL_SECONDS = 600
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Queue list TTL not extended by active head waiter

QUEUE_LIST_TTL_SECONDS (600 s) is only refreshed via pipeline.expire inside enqueue. The refreshHeartbeat method extends the per-ticket key (30 s TTL, refreshed every 10 s) but never touches the queue list key. If the head waiter's execution budget exceeds 10 minutes (ABSOLUTE_MAX_QUEUE_WAIT_MS derives from getMaxExecutionTimeout(), which can be much longer for enterprise async) and no new caller arrives to refresh the list TTL, the queue list expires. Every waiter's next checkHead call sees "missing" (list gone → LINDEX returns nil → script returns "missing") and all proceed to the bucket simultaneously, collapsing FIFO ordering into concurrent bucket racing. Extending the queue list TTL inside refreshHeartbeat (alongside the ticket key) would close this gap.

Comment on lines +53 to +75
*/
const QUEUE_HEAD_POLL_MS = 200

/**
* Sleep for `ms`, resolving early if `signal` aborts. Cleans up its own timer and listener
* so neither leaks. Callers don't need to distinguish an early (aborted) return from a normal
* one — the surrounding wait loop re-checks its budget immediately after and bails when the
* signal has fired. Falls back to a plain sleep when no signal is provided.
*/
function interruptibleSleep(ms: number, signal?: AbortSignal): Promise<void> {
if (!signal) return sleep(ms)
if (signal.aborted) return Promise.resolve()
return new Promise<void>((resolve) => {
const onAbort = () => {
clearTimeout(timer)
resolve()
}
const timer = setTimeout(() => {
signal.removeEventListener('abort', onAbort)
resolve()
}, ms)
signal.addEventListener('abort', onAbort, { once: true })
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 TOCTOU window in interruptibleSleep between signal.aborted check and addEventListener

If the signal fires between the if (signal.aborted) guard at the top and the signal.addEventListener('abort', onAbort, { once: true }) call at the bottom, the abort event will not be delivered to the listener (it already fired before the listener was registered). The sleep then runs to full ms duration instead of resolving early. The call-site comment notes callers re-check their budget after each sleep, so the practical impact is bounded to one extra sleep period (at most HEARTBEAT_REFRESH_INTERVAL_MS = 10 s in the bucket-wait path). Adding a signal.aborted re-check immediately after addEventListener would eliminate the window entirely.

Comment on lines +69 to +74
currentPayload = {
...currentPayload,
groupId: next.id,
workflowId: next.workflowId,
executionId: generateId(),
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Stale enrichmentId carried forward when re-driving a workflow group after an enrichment group

The outer re-drive loop spreads ...currentPayload when building the next iteration's payload, which means enrichmentId from a prior enrichment group persists if the next group is a workflow group (next.enrichmentId is undefined and is never explicitly cleared). The current runWorkflowAndWriteTerminal implementation routes on group.type === 'enrichment' && group.enrichmentId (using the schema-fresh group object, not payload.enrichmentId), so this stale field does not cause incorrect routing today. However, if any future code path reads payload.enrichmentId as a signal for "this is an enrichment run", the mis-attribution would silently trigger the wrong branch. Explicitly setting enrichmentId: next.enrichmentId (or enrichmentId: undefined) when constructing currentPayload would keep the payload consistent with the target group.

@TheodoreSpeaks
Copy link
Copy Markdown
Collaborator Author

Greptile and bugbot issues are legit but not actually that impactful. Will take these in a followup pr.

@TheodoreSpeaks TheodoreSpeaks merged commit fd19470 into main May 27, 2026
31 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant