* improvement(scheduler): raise per-tick claim budget to drain backlog
MAX_CRON_CLAIMS 20 -> 100; reserved workflow/job slots 10/10 -> 50/50.
Throughput was capped at 20 schedules/tick which created a 20+ hour
backlog when due work exceeded ~1 item per cron-second.
* improvement(scheduler): raise per-tick claim budget to 200
Bumps MAX_CRON_CLAIMS 100 -> 200 (workflow/job split 100/100). Pairs
with the fire-and-forget cron Lambda change so per-tick processing
time is no longer bounded by the Lambda's 50s HTTP timeout.
* fix(file-viewer): prevent scroll jump to top during Mothership streaming
- Fix root cause: MarkdownCheckboxCtx.Provider was conditionally rendered,
causing the scroll container to unmount/remount when isStreaming flipped,
resetting scrollTop to 0 on every stream start
- Add useScrollAnchor hook with spacer element to preserve scroll position
when streamed content temporarily shrinks the scroll container
- Linger active session ID on complete to prevent streamingContent→undefined
flicker between consecutive tool calls on the same file
- Gate upsert activation on incoming session having renderable content
- Fix shouldShowStreamingFilePanel to keep panel mounted during linger
- Fix use-chat post-write navigation to work with lingered completed session
- Fix useAutoScroll to check proximity before pinning to bottom on stream start
* fix(file-viewer): preserve session linger in hydrate/upsert and clear spacer after stream
* chore(file-viewer): trim verbose comments to match codebase style
* fix(file-viewer): re-engage auto-scroll when user scrolls back to bottom
* chore(file-viewer): update scroll-anchor tsdoc, remove test separators, add hydrate linger tests
* fix(file-viewer): prevent false re-engage when spacer restoration triggers onScroll
* refactor(file-viewer): extract shouldReengage as pure tested function
Pulls the spacer-guard re-engage condition out of onScroll into an
exported pure function so the false-re-engage invariant (spacer active
→ no re-engage) is covered by automated tests rather than relying on
manual QA. Adds 8 unit tests for shouldReengage alongside the existing
15 for computeSpacerShortage.
* chore(file-viewer): trim verbose inline comments in use-scroll-anchor
* chore(file-viewer): final comment cleanup before merge
* feat(data-drains): add GCS, Azure Blob, BigQuery, Snowflake, and Datadog destinations
* fix(data-drains): address PR review comments
* fix(data-drains): extract sleepUntilAborted, honor abort across all destinations
* fix(data-drains): widen BigQuery projectId max and dedupe parseServiceAccount
* fix(data-drains): tighten GCS bucket contract and expose Azure endpointSuffix
* improvement(data-drains): extract normalizePrefix and buildObjectKey to shared utils
* fix(data-drains): retry BigQuery network errors; tighten Azure accountKey contract
- BigQuery insertAll now wraps the fetch in try/catch inside the retry loop so DNS failures, socket resets, and timeouts are retried with backoff instead of propagating immediately.
- Align azureBlobCredentialsBodySchema with the runtime schema (min 64 / max 120 / base64 regex) so obviously invalid keys are rejected at the API boundary rather than at drain-run time.
* improvement(data-drains): consolidate parseRetryAfter; add Datadog NDJSON line context
- Extract a single parseRetryAfter helper (capped at 30s, returns number | null) into lib/data-drains/destinations/utils.ts and remove the five local copies in bigquery, datadog, gcs, snowflake, and webhook.
- Datadog parseNdjson now wraps JSON.parse in try/catch and surfaces the failing line index, matching BigQuery's parser.
* fix(data-drains): correct Datadog size guard and Snowflake VARIANT limit
- Datadog payload guard now checks the uncompressed size against the 5 MB limit and the wire size against the 6 MB compressed limit, so gzip cannot smuggle an oversized body past the client-side check.
- Snowflake VARIANT limit is 16 MiB (16,777,216 bytes), not 16,000,000 bytes — small payloads between 16 MB and 16 MiB were being rejected unnecessarily.
- Drop the unused apiKey field on Datadog PostInput; the key is already embedded in the prepared request headers.
* improvement(data-drains): consolidate backoffWithJitter into shared utils
Datadog, GCS, and webhook each had byte-identical backoff helpers (BASE 500ms, MAX 30s, jitter ±20%, Retry-After floor). Lift the helper into lib/data-drains/destinations/utils.ts alongside parseRetryAfter and sleepUntilAborted, and drop the per-file copies and their BASE_BACKOFF_MS/MAX_BACKOFF_MS constants.
* fix(data-drains): align destinations with live provider specs
Audited every destination against live AWS/GCS/Azure/BigQuery/Snowflake/
Datadog/webhook docs and applied spec-correctness fixes:
- S3: reserved bucket prefix amzn-s3-demo-, suffixes --x-s3/--table-s3;
metadata byte formula excludes x-amz-meta- prefix per AWS spec
- GCS: reject -./.- adjacency; UTF-8 prefix cap; forbid .well-known/
acme-challenge/ prefix; ASCII-only x-goog-meta-* enforcement
- BigQuery: insertId is 128 chars (not bytes); split DATASET_RE (ASCII)
and TABLE_RE (Unicode L/M/N + connectors); UTF-8 byte cap on tableId
- Snowflake: disambiguate org-account vs legacy locator account formats;
requestId+retry=true for idempotent retries; server-side timeout=600;
default column DATA uppercase to match unquoted canonical form
- Azure: endpoint suffix allowlist (4 sovereign clouds); accountKey
length(88) base64
- Webhook: url max(2048); CRLF/NUL rejection on bearer/secret/sig header
* fix(data-drains): address PR review on snowflake poll + shared NDJSON parsing
- snowflake pollStatement: per-attempt timeout via AbortSignal.any, retry on 429/5xx with Retry-After + jitter
- bigquery parseNdjson error messages now 1-indexed
- consolidate parseNdjson variants into shared parseNdjsonLines/parseNdjsonObjects in utils
* fix(data-drains): per-attempt fetch timeouts in gcs/bigquery, snowflake poll double-sleep
- gcs.fetchWithRetry + bigquery.postInsertAll now use AbortSignal.any with a per-attempt timeout so a hung TCP connection cannot stall the drain
- snowflake.pollStatement skips the next interval sleep when it just slept for retry backoff
* fix(data-drains): bigquery probe timeout + jittered retries, align Snowflake column default UI/docs
- bigquery test() probe now uses AbortSignal.any + per-attempt timeout
- bigquery insertAll retry switches to backoffWithJitter for thundering-herd avoidance
- Snowflake column placeholder + docs say DATA (uppercase) to match the code default
* fix(data-drains): mirror webhook signingSecret min length in form gate
isComplete now requires signingSecret >= 32 to match the contract/runtime
schema so the Save button can't enable on a value that will fail server-side.
* fix(data-drains): validate JSON client-side for Snowflake before binding
Switch Snowflake to parseNdjsonObjects so malformed rows are caught locally
with 1-indexed line numbers instead of failing the whole INSERT server-side.
Re-stringify each parsed object before binding to PARSE_JSON(?).
Drop the now-unused parseNdjsonLines helper.
* fix(data-drains): cross-cutting audit pass against live provider docs
- Azure: bound retryOptions on BlobServiceClient (SDK default tryTimeoutInMs is per-try unbounded; cap at 30s x 5 tries)
- Webhook contract: mirror runtime — signingSecret.max(512), bearerToken.max(4096) + CRLF/NUL refine, signatureHeader charset + CRLF/NUL refine
- S3 (lib + contract): reject bucket names with dash adjacent to dot; require https:// endpoint at the schema layer
- Snowflake: bind original NDJSON line bytes (re-stringifying a JSON.parse'd value loses bigint precision beyond 2^53-1); check pollStatement 200 body for the SQL error envelope (sqlState/code)
- Datadog: entry builder writes defaults first then user attrs then forced ddtags/message so user rows can't clobber routing fields; validate config.tags as comma-separated key:value pairs
- registry.tsx: tighten isComplete predicates to mirror contract minimums (GCS bucket >= 3, Azure containerName >= 3 / accountKey === 88, BigQuery projectId >= 6, Snowflake account >= 3)
* fix(data-drains): force ddsource/service overrides on Datadog entries
Previous fix placed ddsource/service before ...attrs, leaving them clobberable
by a user row field. Per Datadog docs, service + ddsource pick the processing
pipeline, so a drain's routing config must not be overridable per-row. Spread
attrs first, then force all four reserved fields (ddsource, service, ddtags,
message).
* fix(data-drains): preserve row-distinguishing index when BigQuery insertId overflows
Truncating from the left dropped the index suffix, so any overflow would
collapse all rows in a chunk to the same insertId and BigQuery would silently
dedupe them. Path is unreachable today (UUIDs keep raw ~85 chars), but the
overflow branch is now correct: hash the prefix, keep the index intact.
* fix(data-drains): refresh GCS token per retry, tighten Azure key regex
- gcs: rebuild Authorization header per attempt via buildHeaders so token
refresh from google-auth-library kicks in if a 5xx retry crosses the
hour-long token lifetime
- azure_blob: pin account-key regex to {0,2} trailing '=' (base64 of 64
bytes = exactly 88 chars with up to two '=' pad chars)
* fix(data-drains): address bugbot review of 6336948f6
- gcs: allow 1-char dot-separated bucket components (e.g. "a.bucket")
to match GCS naming rules — overall name is 3-63 (or up to 222 with
dots), but per-component minimum is 1 per Google's spec
- bigquery: drain the 401 response body before re-issuing the request
with a refreshed token so undici can return the socket to the
keep-alive pool
- snowflake: hoist getJwt() above the perAttempt timer in
executeStatement so JWT signing doesn't eat the network budget
(matches the order already used in pollStatement)
* fix(data-drains): allow org-account Snowflake identifier with region suffix
The account validation rejected `<orgname>-<acctname>.<region>.<cloud>`
because `ACCOUNT_LOCATOR_RE`'s first segment forbade hyphens, while
`ACCOUNT_ORG_RE` forbade dots. `normalizeAccountForJwt` already handles
this composite form. Widen the first segment of `ACCOUNT_LOCATOR_RE` to
allow hyphens so the boundary contract and the runtime schema accept
what the JWT layer was already designed to process.
* fix(data-drains): drain retryable response bodies in datadog/gcs loops
Mirrors the bigquery 401 fix. Without consuming the body before
sleeping, undici can't return the socket to the keep-alive pool, so
each retry leaks a TCP connection instead of reusing it.
* fix(data-drains): drain snowflake poll bodies on 202 and retryable status
Mirrors the bigquery/datadog/gcs drains. Long async statements can poll
many times against the same connection; without consuming the body
undici can't return the socket to the keep-alive pool, so each iteration
leaks a connection until GC.
* fix(data-drains): consume success bodies; check Snowflake sqlState on 200
- gcs: drain the body on success paths so undici can return the socket
to the keep-alive pool
- snowflake: drain the body on synchronous 200 OK and run the same
sqlState envelope check pollStatement already does — otherwise a
statement-level failure that completes synchronously would silently
return success
* fix(data-drains): drain datadog and bigquery probe success bodies
Same undici keep-alive issue as the prior fixes: postWithRetries
returned the Response on success without draining (callers only read
headers); the BigQuery `test()` probe returned without consuming the
body. Both now drain before returning.
* chore(data-drains): regenerate enum migration as 0206 after staging rebase
* fix(data-drains): cap snowflake poll retries; tighten datadog tags min length
* feat(table): live cell updates via SSE + per-table event buffer
Replaces the polling-based row refetch with a push-based SSE stream that
patches the React Query cache directly as cell-state events arrive.
Architecture:
- New per-table event buffer in apps/sim/lib/table/events.ts. Redis sorted-set
with monotonic eventId, 1h TTL, 5000-event cap, in-memory fallback. Modeled
after apps/sim/lib/execution/event-buffer.ts but stripped of complexity
tables don't need (no per-execution lifecycle, no id-batching, no write
queue serialization). ~150 lines instead of 700.
- writeWorkflowGroupState appends a fat event after each successful 'wrote'.
Status transitions carry executionId + jobId; terminal/partial transitions
also include the new output values inline so the client can patch row data
without a follow-up refetch.
- New SSE route at /api/table/[tableId]/events/stream?from=<lastEventId>.
Replays from buffer on connect, polls at 500ms (mirrors workflow execution
stream), heartbeat every 15s, signals 'pruned' if the caller fell off the
back of the buffer.
- Client hook useTableEventStream subscribes via EventSource. Reconnect-resume
with last-seen eventId. On 'pruned', invalidates the rows query and resumes
from the new earliest. Cache patches walk every cached query under
rowsRoot(tableId) so filter/sort variants all stay live.
- Removes refetchInterval from useTableRows and the per-page polling effect
from useInfiniteTableRows. React Query's refetchOnWindowFocus +
refetchOnReconnect cover the durability gap if any push is dropped.
Out of scope:
- Bulk-cancel events (cancellation path is being redesigned separately).
- Generalizing the workflow event-buffer module to a shared primitive (defer
until a third use case appears; for now the table buffer is the simpler
cousin of the workflow one).
* fix(table): drop run-mutation refetch so SSE patches aren't overwritten
useRunColumn.onSettled was canceling in-flight queries and invalidating the
rows query — leftover behavior from the polling era. With the SSE stream
now keeping the cache live via incremental patches, this refetch races the
stream and snaps the cache back to whatever DB shows at the refetch moment,
which can lag the just-arrived queued/running events. Cells appeared stuck
on the optimistic 'pending' even though the SSE was delivering the real
transitions.
* chore(table): simplify SSE plumbing — reuse helpers, drop dead polling code
- Reuse snapshotAndMutateRows for SSE cache patches instead of reimplementing
the page-walk + cache-shape detection. Adds a {cancelInFlight: false} opt
for the SSE caller (mutations still cancel as before).
- Drop client-side type duplication in use-table-event-stream — import
TableEvent and TableEventEntry from lib/table/events directly.
- Drop the now-dead mergePagePreservingIdentity + rowEqual from tables.ts;
their only caller was the polling effect that was removed earlier.
- Drop the defensive try/catch around appendTableEvent in cell-write — the
function is documented as never-throwing (returns null on failure).
- Combine INCR + ZADD into one Lua eval in events.ts. Halves Redis RTT per
cell-write. Lua returns the new eventId; the script splices it into the
pre-built entry JSON.
- Trim refs to plain let bindings inside the effect; trim stale
comments referencing the old polling implementation.
* fix(table): address PR review on SSE buffer
- TTL-expiry silent miss: when all keys expire, hgetall(meta) returns empty
so earliestEventId is undefined and the prune branch was skipped. Reconnect
with non-zero afterEventId now checks the seq counter — its absence (TTL
expired) signals pruned so the client refetches. Memory fallback mirrors.
- Unbounded ZRANGEBYSCORE: cap reads at TABLE_EVENT_READ_CHUNK = 500 events
per call. The route's 500ms poll loop drains chunks across ticks instead of
flushing 5000 entries (multi-MB) in one tick after a long disconnect.
- Pruned handler closes EventSource client-side: server-side close was firing
onerror and routing through the 500ms backoff path. Now we close
proactively, reset the reconnect attempt counter, and reconnect immediately
from the new earliest.
* Cross env copilot
* Force deploy
* Run migration
* Updates
* Fix migration
* Redeploy
* Make dev db push
* restore old migs
* Cross env copilot
* Add custom tools, skills, mcps to mothership
* Update migration
* Fix migs
* UPdate
* Fix types
* Fix
---------
Co-authored-by: Theodore Li <theo@sim.ai>
* fix(security): authorize MCP subagent IDs, oauth workspace, credential admin demotion
- handleSubagentToolCall and handleDirectToolCall now authorize user-supplied
workflowId/workspaceId via authorizeWorkflowByWorkspacePermission /
ensureWorkspaceAccess before forwarding downstream; resolvedWorkspaceId is
derived from the authorized workflow record instead of trusted from the body
- executeOAuthGetAuthLink verifies caller membership (write level) on the
target workspaceId before generating the OAuth link or writing
pendingCredentialDraft, closing the cross-workspace credential injection path
- POST /api/credentials/[id]/members wraps role updates in a transaction that
counts active admins and rejects demotion of the last admin (mirrors the
existing DELETE guard in the same file)
- GET /api/credentials/[id]/members returns uniform 404 for both missing and
inaccessible credentials to remove the existence oracle
* fix(security): address PR review — active-status guard, FOR UPDATE locks, workspaceId propagation
- credentials/members POST: add `current.status === 'active'` check to the
last-admin demotion guard so re-inviting a revoked admin as a non-admin role
no longer incorrectly hits the "Cannot demote the last admin" path
- credentials/members POST+DELETE: add `.for('update')` to the active-admin
count SELECT inside both transactions to serialize concurrent demotions and
eliminate the admin-count TOCTOU race under Postgres READ COMMITTED
- credentials/members POST: also lock the member row itself with `.for('update')`
so the role+status read and the subsequent UPDATE are atomic
- mcp/copilot handleDirectToolCall: thread the DB-verified workspaceId from the
authorization result into prepareExecutionContext instead of relying on
user-supplied args
- oauth handler: fix error message to mention both workspaceId and userId when
either is missing from the execution context
* fix(oauth): persist rotated Microsoft refresh tokens
Microsoft Entra rotates refresh tokens on every refresh and expects clients to replace the stored token with the new one. The Microsoft provider config was missing supportsRefreshTokenRotation, so the rotated refresh_token returned by Azure AD was silently discarded and the original token from initial OAuth connect was reused indefinitely — causing periodic 'Failed to refresh access token' errors for Excel, Teams, Outlook, OneDrive, SharePoint, Planner, AD, and Dataverse integrations.
* test(oauth): cover hyphenated Microsoft service IDs in rotation test
* fix(security): close IDOR gaps in OAuth credential and execution authorization
Routes that called resolveOAuthAccountId followed by a conditional
workspace permission check (only run when workspaceId was set) silently
skipped all ownership validation on the legacy account-ID fallback path.
Any authenticated user could supply a raw account.id to access another
tenant's OAuth credentials.
- Replace resolveOAuthAccountId + conditional perm check with
authorizeCredentialUse in: auth/oauth/wealthbox/item, tools/gmail/label,
tools/onedrive/files, tools/onedrive/folder, tools/outlook/folders,
tools/wealthbox/item (routes 1, 3-7)
- Add authorizeCredentialUse ownership gate before resolveVertexCredential
in providers/route.ts (route 2)
- Add verifyFileAccess check on the user-supplied file key before
downloadFileFromStorage in tools/wordpress/upload (route 8)
- Add workflowId param to PauseResumeManager methods
(enqueueOrStartResume, beginPausedCancellation, completePausedCancellation,
blockQueuedResumesForCancellation, clearPausedCancellationIntent,
getPausedCancellationStatus, processQueuedResumes) and filter all
pausedExecutions lookups by workflowId so callers cannot act on another
tenant's paused execution by supplying a foreign executionId (route 9)
- Update all call sites (cancel, resume, poll routes) to pass workflowId
* fix(security): close verifyFileAccess bypass, thread workflowId to processQueuedResumes, fix log level
- Fail closed in WordPress upload when userFile.key is present but authResult.userId is absent, preventing silent bypass of ownership check via JWT fallback path
- Thread workflowId into processQueuedResumes in the async resume error-recovery path and in pause-persistence.ts to close residual cross-tenant gap
- Change logger.error to logger.warn for credential access denial in OneDrive folder route to match all other routes in this PR
* fix(security): thread workflowId through all processQueuedResumes call sites
Closes residual cross-tenant IDOR gap where processQueuedResumes was called
without a workflowId scope in persistPauseResult, startResumeExecution (success
and error paths), and clearPausedCancellationIntent. workflowId was already in
scope at each site — this wires it through to the existing optional parameter.
* fix(security): remove any types, drop extraneous comments, normalize caught errors
- catch (error: any) → catch (error) + toError(error).message in resume and cancel routes
- Remove what-not-why inline comments from wordpress upload and onedrive/files routes
- Remove redundant debug-only item breakdown log and the file-IDs log in onedrive/files
- Trim extraneous DAG-edge comments from updateSnapshotAfterResume in HITL manager
* fix: use logger.warn for credential access denial in outlook folders route
* fix(security): make workflowId required in all HITL pause/resume methods
All 7 method signatures (processQueuedResumes, enqueueOrStartResume,
beginPausedCancellation, completePausedCancellation, blockQueuedResumesForCancellation,
clearPausedCancellationIntent, getPausedCancellationStatus) previously accepted
workflowId as optional. Every call site already supplies it — making it required
closes the vulnerability at the type level so future callers cannot accidentally
omit tenant scoping and silently fall back to an unscoped DB query.
* fix(security): thread workflowId through internal HITL cancellation calls and remove dead branches in credential-access
* fix(security): harden workflowId scoping and file key guard
Replace falsy workflowId checks in PauseResumeManager (all methods now
unconditionally apply the workflowId WHERE clause, preventing empty-string
bypass). Flip WordPress upload file guard from truthy key check to explicit
non-empty validation so key:"" fails closed with a 404 instead of silently
skipping access control.
* fix: address SSRF and token-leakage security vulnerabilities
- Azure TTS SSRF: validate region against /^[a-z][a-z0-9-]{1,30}[a-z0-9]$/
in both the contract (tts.ts) and runtime guard in synthesizeWithAzure,
preventing user-supplied region from redirecting requests to arbitrary hosts
- HubSpot token in logs: remove fullResponse from logger.info call;
log only non-sensitive metadata (hub_id, hub_domain, user_id) instead
of the full introspection response which included the access token
- Wealthbox account takeover: replace hardcoded email with per-user identity
by fetching /v1/users/me; fall back to token-derived stable identifier
so distinct Wealthbox users no longer share the same email address
- Shopify SSRF: apply shopifyShopDomainSchema (.myshopify.com allowlist)
to shopDomain from cookie before using it to build the fetch URL
* fix(wealthbox): correct getUserInfo endpoint, auth header, and stable identity
- Bug 1: Change API endpoint from /v1/users/me to /v1/me (correct Wealthbox API path)
- Bug 2: Replace ACCESS_TOKEN header with Authorization: Bearer <token> (standard OAuth 2.0)
- Bug 3: Remove generateId() from returned id (was non-deterministic, caused duplicate accounts);
use refresh token (stable, long-lived) instead of access token (rotates every ~2 hours)
as the hash source for the fallback identity; return null if no token is available
* fix(security): hash wealthbox fallback token identity, guard undefined userId
- Replace base64 encoding with SHA-256 hash for fallback token-derived identity
so raw token bytes are never stored in the DB
- Return null early when Wealthbox API response lacks an id field to prevent
all such users colliding on the wealthbox-undefined account
* fix(auth): replace stale wealthbox userInfoUrl placeholder with actual endpoint
The dummy URL comment was rendered obsolete when getUserInfo was updated
to fetch from api.crmworkspace.com/v1/me. Align userInfoUrl with the real
endpoint used in the getUserInfo implementation.
* fix(auth): append generateId() suffix to Wealthbox account IDs to match codebase pattern
All other providers use `${stableId}-${generateId()}` so the account.create.after
hook can strip the UUID suffix, find stale sibling rows, and migrate credential FKs.
Without the suffix the migration logic is skipped and reconnections would hit
duplicate key conflicts instead of gracefully updating credentials.
Adds the enterprise data-drains docs page with two screenshots, a
search bar over the drains table, and a UI cleanup pass on the
settings component (size-*, useMemo removal, text-sm fixes).
The previously-proposed env-var reference feature has been dropped —
drain credentials remain raw values, encrypted at rest by the
existing pipeline.
* fix(settings): accurate View navigation after restore in recently deleted
Files now deep-link to /files/{id} (was /files), and folders expand
the restored folder + its parent chain in the sidebar before navigating
to /w so the user actually lands on the item they restored.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix: fall back to archived folders for parent-chain lookup
The restored folder may not be in the active folders cache yet when
View is clicked (the invalidation+refetch fires from onSettled, after
onSuccess surfaces the View button). Merge archived folder data — where
the restored item still lives — into the lookup map so the expansion
loop can always resolve the folder and walk its parent chain.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore(styling): canonicalize size-* shorthand for equal height/width
Document the size-* shorthand as the canonical pattern across CLAUDE.md,
AGENTS.md, .claude rules, .cursor + .agents commands, and the emcn
design-review skill. Default icon size is size-[14px]. Treat
h-[Npx] w-[Npx] and h-N w-N pairs as refactor targets.
Also migrate the remaining occurrences in recently-deleted.tsx.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* fix(execution): cap isolate memory at 128MB and recycle workers every 100 executions
* fix(execution): set IVM_MAX_EXECUTIONS_PER_WORKER env default to 100
* fix(execution): raise MAX_EXECUTIONS_PER_WORKER from 100 to 200
* fix(execution): update memory limit error messages from 256 MB to 128 MB
* fix(tables): fix bulk ops truncation for tables larger than one page
Bulk operations (column-header delete, select-all copy/cut/delete/run)
were silently truncated to the first 1000 rows because handlers only
iterated the loaded pages from useInfiniteQuery.
Fix:
- Extract tableRowsInfiniteOptions factory (infiniteQueryOptions) so
the hook and imperative drain share the same typed cache key
- Add background drain via useEffect watching hasNextPage/isFetchingNextPage
— chains fetchNextPage until getNextPageParam returns undefined
- Add ensureAllRowsLoaded to use-table: reads cache via getQueryData +
calls fetchNextPage in a while loop until the last page is partial
- Await ensureAllRowsLoaded at every kind:'all' bulk-op entry point in
table-grid (column delete, copy, cut, action-bar delete/run)
- Add chunkBatchUpdates to send updates in MAX_BULK_OPERATION_SIZE=1000
chunks so server validation never rejects oversized batches
- Fix undo-redo: make executeAction async and chunk clear-cells,
update-cells, and delete-column cell-restore with mutateAsync loops
Tests: 41 passing across use-table, tables queries, and use-table-undo
* chore(tables): remove extraneous comments
* fix(tables): add missing useEffect import; chunk range-selection delete and cut
* fix(tables): add hasRunningGroupExecution and import it
* fix(tables): define mergePagePreservingIdentity helper used in polling cache merge
* fix(tables): define ROWS_POLL_INTERVAL_WHILE_RUNNING_MS constant used in polling loop
* fix(tables): capture rowSel before await in delete handler; handle clipboard NotAllowedError
* fix(tables): abort cut on clipboard NotAllowedError to prevent silent cell deletion
* fix(tables): push undo before chunkBatchUpdates to survive partial chunk failures
* fix(tables): use text input for number cells; idle poll backoff; csv error toast; column-cut drain
* fix(tables): audit fixes — column-copy drain, polling scope, merge identity
- Fix polling tick: move Promise.all inside else-branch so dirty[] stays in
scope; keep hasDirty=true during active mutations so the short interval
fires while chunked batch-updates are in flight
- Add isColumnSelectionRef branch to handleCopy (mirrors handleCut fix):
column-header Cmd+C now drains all pages before building clipboard content
- Replace String(updatedAt) comparison in mergePagePreservingIdentity with
Date.getTime() equality — handles ISO vs +00:00 timezone variants
- Remove redundant batchUpdates.length > 0 guards at chunkBatchUpdates
callsites (empty-array case is handled inside the function)
- Export _mergePagePreservingIdentity for unit testing
- Add 6 unit tests covering mergePagePreservingIdentity edge cases
* improvement(tables): cleanup — extract components, stabilize callbacks, fix ref sync
* improvement(tables): remove polling, eager drain, and parallelize batch updates
- Drop the per-page polling loop — SSE stream already patches execution
cell state in real time and invalidates on buffer prune; polling was
redundant and burned CPU/network on every open table
- Remove eager mount drain (fetchNextPage loop in use-table.ts); scroll
handler and ensureAllRowsLoaded handle progressive/on-demand loading
- Parallelize chunkBatchUpdates with a 3-worker pool instead of serial
chunks, reducing bulk-op round-trips by ~3x
- Delete mergePagePreservingIdentity and its tests (no longer called)
* chore(tables): remove stale comments and dead defensive code
- Fix chunkBatchUpdates JSDoc to reflect parallel dispatch (was "sequentially")
- Inline CHUNK_CONCURRENCY=3, single-use constant needs no abstraction
- Drop stale "Polls while any cell is in flight" from useTableRows JSDoc
- Remove two generic "Validation errors surfaced by caller" comments
- Remove ASCII separator line from workflow group mutations section
- Remove dead `if (!variables) return` guard in useImportCsvIntoTable onSettled
(TanStack v5 always provides variables to onSettled)
* fix(tables): run-all selection sends all rows to server, not just loaded pages
When rowSelection.kind === 'all', selectedRunScope now flags allRows: true.
The action-bar run handlers pass rowIds: undefined to the server when allRows
is set, matching the server contract (missing rowIds = run all eligible rows).
Stop likewise routes through scope: 'all' instead of per-row cancels.
Previously, selecting all rows and clicking Run would silently only run the
rows loaded in the current infinite-query cache (potentially one page of 1000
on a 5000-row table).
* test(tables): remove background-drain describe block (behavior intentionally removed)
* fix(tables): surface CSV import error toast; remove dead hasRunningGroupExecution
* fix(tables): make runWithoutRecording async-aware so undoRedoInProgress covers full undo execution
* feat(tables): render URL cells with favicon and clickable link
* feat(tables): clickable URL cells with favicons using tldts
* fix(security): enforce workspace scope on workflow middleware and validate shopify shop domain
- validateWorkflowAccess now rejects workspace-scoped API keys whose
workspaceId doesn't match the workflow's workspace, closing a boundary
leak across /api/workflows/[id]/{log,paused,status} and
/api/resume/[workflowId]/[executionId]/[contextId]
- shopify authorize route now validates the resolved shop domain against
shopifyShopDomainSchema before proceeding
- adds middleware tests covering workspace/personal/session auth paths
* fix(shopify): disallow trailing hyphen in shop subdomain regex
* fix(shopify): align shop domain regex with shopify handle rules (3-60 lowercase, no edge hyphens)
* fix(security): widen shopify subdomain regex to allow up to 63 chars
Shopify and RFC 1123 allow labels up to 63 chars; the previous
{1,58} quantifier capped the subdomain at 60 chars and rejected
valid 61–63 char shops with a 400.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* fix(uploads): allow image/video/audio attachments in mothership presigned route
The mothership branch of the presigned upload route called validateFileType,
which only permits SUPPORTED_DOCUMENT_EXTENSIONS — rejecting PNG screenshots
and other media users have always been able to attach via the legacy
/api/files/upload mothership branch. Introduce validateAttachmentFileType,
backed by the union of document, code, image, audio, and video extensions,
and wire it into the mothership branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(uploads): allow non-document execution outputs in presigned route
The execution branch of the presigned upload route called validateFileType,
which only permits documents — but workflow execution outputs are arbitrary
by design (images, audio, video, code). The legacy /api/files/upload
execution branch had no docs-only gate, so the staging refactor to
presigned PUTs regressed parity. Switch execution to validateAttachmentFileType
to match prior behavior.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(uploads): add coverage for attachment validator and dedupe extensions
- Dedupe SUPPORTED_ATTACHMENT_EXTENSIONS (webm appears in both audio
and video lists)
- Reuse SUPPORTED_ATTACHMENT_EXTENSIONS in /api/files/upload to avoid
drift with the presigned route
- Add unit tests for validateAttachmentFileType
- Add presigned route tests covering mothership/execution/knowledge-base
validator selection and permission gating
* fix(uploads): restore SUPPORTED_IMAGE_EXTENSIONS import in upload route
Build broke because biome auto-fix collapsed my multi-import edit and
dropped SUPPORTED_IMAGE_EXTENSIONS, which is still referenced for the
generic-MIME image fallback at line 298.
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* chore(deps): audit and clean up dependencies
- Remove unused: chalk, chart.js, dotenv, encoding, entities, thread-stream, uuid, @opentelemetry/exporter-jaeger, critters, marked, redis, soap
- Replace soap with hand-rolled Workday SOAP client
- Migrate marked to unified pipeline for inbox responses
- Bump zustand v5, @react-email/*
- Align all @aws-sdk/* to 3.1032.0
- Move type-only deps to devDependencies
- Remove duplicate drizzle-orm/postgres overrides
* fix(workday): coerce SOAP scalar strings to typed booleans/numbers
- XML parser returns leaf text as strings; `!"false"` evaluated to
`false`, causing all organizations to report `isActive: false`
- Add parseSoapBoolean and parseSoapNumber helpers and apply at consumer
sites (Inactive, Total_Results)
- Drop unused service/soapAction fields from WD_OPERATIONS map
* fix(workday): coerce compensation amounts and guard Date marshaling
- get-compensation returned Amount/Per_Unit_Amount/Individual_Target_Amount
as strings (XML leaf text), violating the tool's number contract
- Coerce via parseSoapNumber and widen plan type to number | string
- Add defensive Date branch in marshal() so Date inputs serialize as
ISO 8601 instead of String(date)
* fix(logs): include subfolders when filtering logs by folder
* fix(logs): use pop() for O(1) dequeue in folder BFS
* fix(logs): move folder expansion to server-only module to fix client bundle build
* feat(peopledatalabs): add People Data Labs integration
Add 11 PDL operations: person enrich/identify/search/bulk, company
enrich/search/bulk/clean, location/school cleaners, and autocomplete.
All endpoints, params, and response shapes verified against official
PDL docs (scroll_token pagination, top-level likelihood on company
enrich, per-item likelihood on bulk company, full autocomplete field
enum).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(peopledatalabs): narrow conditions for fields not used by every operation
- min_likelihood now only shows for pdl_person_enrich (Person Identify ignores it)
- ticker, pdl_id, company_location now only show for pdl_company_enrich
(Company Cleaner only accepts name/website/profile)
Addresses Greptile P1 review on PR #4513.
* fix(peopledatalabs): scope param renames to their operation
Param renames (company_profile→profile, company_location→location,
school_*→*, bulk_*_requests→requests, autocomplete_size→size, etc.)
now run only when the matching operation is selected, and stale
alternate-operation values are stripped from the request. This
prevents values left over from a prior operation switch from leaking
into the current API call (e.g. a company LinkedIn URL overwriting
a person profile, or a stale search size overwriting autocomplete
size).
Addresses Cursor Bugbot review on PR #4513.
* fix(peopledatalabs): use currentColor for icon fill
The PeopleDataLabsIcon was hardcoded to white, leaving it invisible
on light backgrounds when rendered outside its bgColor container
(e.g., search results, menus, docs). Switch to currentColor so it
inherits the surrounding text color.
Addresses Cursor Bugbot review on PR #4513.
* fix(peopledatalabs): scope shared fields (profile/location/name/website) per operation
The block has subBlocks whose raw IDs collide with PDL API param names
(profile, location for person; name, website for company). Their values
persist across operation switches even though the UI hides them, so a
person LinkedIn URL could leak into a Company Enrich request, etc.
Reset these shared targets and repopulate them only from inputs that
belong to the active operation.
Addresses Greptile P1 review on PR #4513.
* fix(peopledatalabs): scope `size` to search and autocomplete operations
`size` is shared by the person/company search subBlock and the
autocomplete_size alias. The previous logic still forwarded a stale
search `size` to operations that don't accept it (e.g. enrich, clean,
identify), and the autocomplete branch only cleared it when
autocomplete_size was unset. Reset `size` up front and only repopulate
it for the three operations that actually accept it.
Found via final integration audit of PR #4513.
* fix(peopledatalabs): restore `location` for Person Identify
Person Identify's tool accepts `location`, and the subBlock is shown
for both Enrich and Identify, but the prior reset only repopulated
`result.location` for Enrich — so any value entered on Identify was
silently dropped before reaching the API.
Addresses Greptile P1 review on PR #4513.
* fix(peopledatalabs): align endpoints + outputs with PDL API
- person_identify: short-circuit on PDL 404 (no-match), matching
the person_enrich pattern
- company_search: drop unsupported `dataset` param (PDL company
search docs do not list it)
- block: expose `min_likelihood` for `pdl_company_enrich` (PDL
Company Enrichment supports min_likelihood)
- location_clean: surface `subregion`; drop phantom `latitude`/
`longitude` (PDL only returns `geo` as a "lat,lon" string)
- school_clean: surface `domain` and `location_continent` from
the nested `location` object
- docs icon: switch fill to `currentColor` so the icon renders
on light backgrounds
* fix(peopledatalabs): restore `name` for Person Enrich / Identify
The shared `name` reset at the top of `tools.config.params` was
only repopulated for the company-side operations, so any
programmatic `name` input to `pdl_person_enrich` or
`pdl_person_identify` was silently dropped. Both PDL endpoints
accept `name` as a full-name match parameter.
* fix(peopledatalabs): restore `name` for Person Enrich
Add the `name` parameter to `PdlPersonEnrichParams`, the tool's
params definition, and the URL builder. PDL Person Enrichment
accepts `name` as a full-name match alternative to first_name +
last_name; without it, programmatic `name` input was silently
dropped before reaching the API.
* fix(peopledatalabs): isolate company `name` UI from person ops
Rename Company Name subBlock id from `name` to `company_name` so a
stale company value can't leak into Person Enrich/Identify when the
user switches operations.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(peopledatalabs): honor programmatic inputs for clean_location/school
`pdl_clean_location` and `pdl_clean_school` were only restoring values
from UI subBlock IDs (`clean_location_input`, `school_*`). Programmatic
callers using the declared `location`/`name`/`website`/`profile` inputs
had their values dropped after the shared-field reset. Add fallbacks so
both UI and programmatic inputs flow through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(peopledatalabs): programmatic input fallbacks + required autocomplete text
- Company Enrich and Clean Company now fall back to programmatic
`params.profile` / `params.location` when the UI-scoped
`company_profile` / `company_location` are absent. Mirrors the
fallback pattern already used for `name`.
- Autocomplete `text` subBlock is now required when operation is
autocomplete — PDL requires it for nearly all field values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* feat(table): live cell updates via SSE + per-table event buffer
Replaces the polling-based row refetch with a push-based SSE stream that
patches the React Query cache directly as cell-state events arrive.
Architecture:
- New per-table event buffer in apps/sim/lib/table/events.ts. Redis sorted-set
with monotonic eventId, 1h TTL, 5000-event cap, in-memory fallback. Modeled
after apps/sim/lib/execution/event-buffer.ts but stripped of complexity
tables don't need (no per-execution lifecycle, no id-batching, no write
queue serialization). ~150 lines instead of 700.
- writeWorkflowGroupState appends a fat event after each successful 'wrote'.
Status transitions carry executionId + jobId; terminal/partial transitions
also include the new output values inline so the client can patch row data
without a follow-up refetch.
- New SSE route at /api/table/[tableId]/events/stream?from=<lastEventId>.
Replays from buffer on connect, polls at 500ms (mirrors workflow execution
stream), heartbeat every 15s, signals 'pruned' if the caller fell off the
back of the buffer.
- Client hook useTableEventStream subscribes via EventSource. Reconnect-resume
with last-seen eventId. On 'pruned', invalidates the rows query and resumes
from the new earliest. Cache patches walk every cached query under
rowsRoot(tableId) so filter/sort variants all stay live.
- Removes refetchInterval from useTableRows and the per-page polling effect
from useInfiniteTableRows. React Query's refetchOnWindowFocus +
refetchOnReconnect cover the durability gap if any push is dropped.
Out of scope:
- Bulk-cancel events (cancellation path is being redesigned separately).
- Generalizing the workflow event-buffer module to a shared primitive (defer
until a third use case appears; for now the table buffer is the simpler
cousin of the workflow one).
* fix(table): drop run-mutation refetch so SSE patches aren't overwritten
useRunColumn.onSettled was canceling in-flight queries and invalidating the
rows query — leftover behavior from the polling era. With the SSE stream
now keeping the cache live via incremental patches, this refetch races the
stream and snaps the cache back to whatever DB shows at the refetch moment,
which can lag the just-arrived queued/running events. Cells appeared stuck
on the optimistic 'pending' even though the SSE was delivering the real
transitions.
* chore(table): simplify SSE plumbing — reuse helpers, drop dead polling code
- Reuse snapshotAndMutateRows for SSE cache patches instead of reimplementing
the page-walk + cache-shape detection. Adds a {cancelInFlight: false} opt
for the SSE caller (mutations still cancel as before).
- Drop client-side type duplication in use-table-event-stream — import
TableEvent and TableEventEntry from lib/table/events directly.
- Drop the now-dead mergePagePreservingIdentity + rowEqual from tables.ts;
their only caller was the polling effect that was removed earlier.
- Drop the defensive try/catch around appendTableEvent in cell-write — the
function is documented as never-throwing (returns null on failure).
- Combine INCR + ZADD into one Lua eval in events.ts. Halves Redis RTT per
cell-write. Lua returns the new eventId; the script splices it into the
pre-built entry JSON.
- Trim refs to plain let bindings inside the effect; trim stale
comments referencing the old polling implementation.
* fix(table): address PR review on SSE buffer
- TTL-expiry silent miss: when all keys expire, hgetall(meta) returns empty
so earliestEventId is undefined and the prune branch was skipped. Reconnect
with non-zero afterEventId now checks the seq counter — its absence (TTL
expired) signals pruned so the client refetches. Memory fallback mirrors.
- Unbounded ZRANGEBYSCORE: cap reads at TABLE_EVENT_READ_CHUNK = 500 events
per call. The route's 500ms poll loop drains chunks across ticks instead of
flushing 5000 entries (multi-MB) in one tick after a long disconnect.
- Pruned handler closes EventSource client-side: server-side close was firing
onerror and routing through the 500ms backoff path. Now we close
proactively, reset the reconnect attempt counter, and reconnect immediately
from the new earliest.
* improvement(table): persist SSE lastEventId in sessionStorage
Tab refresh / navigate-away-and-back now resume the stream from where the
previous mount left off instead of replaying from from=0. Mirrors the
useExecutionStream pattern (saveExecutionPointer / loadExecutionPointer).
First-ever mounts and new tabs still start at 0 — sessionStorage is
per-tab, so the safe default applies. On a 'pruned' fallback the new
earliestEventId is also persisted so the next reconnect starts there.
* fix(table): include runningBlockIds + blockErrors in SSE event payload
The cell renderer's 'queued' vs 'running' vs 'pending-upstream' decision
reads exec.runningBlockIds + exec.blockErrors. Without those fields the
inFlight branch falls through to 'pending-upstream' (amber Pending pill)
even when the worker has already written status=running. The worker writes
both fields to DB; the SSE event was stripping them. Thread them through
events.ts → cell-write.ts → use-table-event-stream.ts.
* fix(table): show value once column output has landed mid-run
The cell renderer treated any `status: 'running'` event as in-flight,
even when the column's own output had already been written. During a
multi-block group run, partial-write events for a later block carry
the earlier block's outputs but tag only the later block as running
— that flipped the finished column back to the amber Pending pill
until the terminal `completed` event arrived.
Re-order the priority chain so the column's value wins over
`pending-upstream`. Active re-run of the column itself
(`blockRunning`) still wins over the stale value, so a re-run on a
previously-completed cell still surfaces the running pill before the
new value overwrites.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(table): address PR review nits on tables.ts
- Merge duplicate JSDoc on snapshotAndMutateRows into a single block
- Remove unused useQueryClient() calls from useTableRows and
useInfiniteTableRows (leftover from polling-era code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(table): apply biome formatting fixes
CI lint job flagged import order in two files and over-wrapped union
in lib/table/events.ts. No behavior change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(uploads): switch mothership uploads to presigned PUT pattern
* fix(uploads): drop unreachable size guard in mothership presigned branch
* improvement(uploads): migrate profile-picture and workspace-logo uploads to presigned PUT
* improvement(uploads): migrate workflow file-upload sub-block to presigned PUT
* improvement(uploads): migrate execution-trigger file uploads to presigned PUT
* fix(uploads): tighten permission checks and fix multipart customKey for mothership/execution
* fix(uploads): require workspace write+ for execution presigned, admin-only for workspace-logos, suppress doubled error toast
* fix(uploads): skip per-file invalidation in batch + extract shared API fallback
- Add skipInvalidation flag to useUploadWorkspaceFile; file-upload sub-block now invalidates once after the batch instead of per file
- Extract uploadViaApiFallback to lib/uploads/client/api-fallback.ts (DRY across 3 hooks)
* fix(tables): optimistic updates for column delete/update
Add onMutate/onError to useDeleteColumn and useUpdateColumn so column
deletes feel instant on large tables (no flash-back during the JSONB
rewrite) and concurrent type changes don't race the in-flight delete's
invalidation.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(tables): toast on delete-column failure, case-insensitive row cleanup, rename row-data keys
Address greptile review:
- useDeleteColumn now toasts on non-validation errors so users see when
the column "snaps back" after a server/network failure
- Row data cleanup matches keys case-insensitively in both useDeleteColumn
and useUpdateColumn so a column stored as "Age" is cleaned even when
the request uses "age"
- useUpdateColumn now migrates row-data keys when updates.name is set,
preventing blank cells during the server round-trip on a rename
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* fix(hunter): align tools, block, and outputs with Hunter.io v2 API spec
* fix(hunter): match documented Discover response and coerce numeric employees
* improvement(sandbox): upgrade pptx/docx/pdf bootstrap with image helpers, MIME guards, and 256 MB isolate limit
* fix(sandbox): strict MIME allowlist and nullish coalescing in docx addImage
* fix(sandbox): validate required opts in pdf drawImage to prevent silent origin placement
* fix(sandbox): throw on malformed data URI in docx addImage
* fix(sandbox): prevent opts from clobbering computed ImageRun data/type/transformation
* fix(sandbox): prevent opts from clobbering fetched data in pptx addImage
* fix(sandbox): validate required opts in pptx addImage
* fix(sandbox): remove silent image/png fallback in docx addImage MIME parsing
* fix(sandbox): consistency and cleanup pass on doc-gen tasks and worker
- DOCX addImage: upfront width/height validation (matches PDF/PPTX pattern)
- PDF embedImage: remove dead Buffer ternary; drop redundant size guard already enforced in getFileBase64
- isolated-vm-worker: add friendly MemoryLimitError branch in both execute paths so OOM produces a clear message instead of a raw V8 error
* improvement(apollo): align tools and block with Apollo API docs
* improvement(apollo): fix tool outputs to match Apollo API response shapes
* chore(apollo): regenerate docs for output changes
* fix(apollo): address PR review comments
* fix(apollo): allow skipped_contact_ids as hash per Apollo docs
* docs
* fix(apollo): add runtime guard for account_bulk_update empty body
* fix(apollo): require contact_attributes for bulk_update
* fix(apollo): add subblock id migrations for renamed opportunity fields
* fix(apollo): tighten account_bulk_update guard and accept object attrs
* fix(apollo): require contact_ids with object-form contact_attributes
* docs(apollo): clarify contact_bulk_update parameter requirements
* fix(apollo): handle flat and wrapped contact response shapes
* validate
* fix(apollo): mirror bulk_update guard, preserve update fields in migration, expose account_bulk_create options
* fix(apollo): don't clobber user contact_attributes in migration; simplify task_create created flag
* fix(apollo): drop undocumented task type, preserve mixed-array IDs, migrate note→task_notes
* fix(apollo): align tools and block with live API docs
Final pass over the Apollo integration after a per-tool forensic audit
against Apollo.io docs. Notable fixes:
- organization_enrich: GET+querystring -> POST+JSON body (canonical, non
master-key)
- organization_bulk_enrich: ?domains[]= -> JSON body { organizations }
- people_search: declare/forward organization_num_employees_ranges; fix
contact_email_status placeholder ("likely to engage", with spaces)
- account_bulk_create: surface failed_accounts and failed count
- contact_bulk_create: expand documented per-contact fields (CRM IDs,
phone_numbers, contact_emails, typed_custom_fields, etc.)
- sequence_add_contacts: surface remaining documented filter params
- task_create: confirm wire field name (note) and remap from task_notes
- types: tighten params/responses for the above
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs
* fix(apollo): add _removed_* migrations for retired opportunity subblocks
* fix(apollo): expose webhook_url subblock for people enrich phone reveal
* fix(apollo): drop colliding account_ids migration, enforce contact bulk limit, expose async toggle for accounts
* fix(apollo): cap account_attributes at 1000 in bulk update
* fix(apollo): drop bare-id merging in bulk update migration to avoid empty attribute objects
* fix(apollo): reject ambiguous account/contact_ids + array-form attributes
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* fix(table): don't let parallel queued stamp overwrite a worker that already started
Stamps fire in chunks of 20 via Promise.all, so queued writes race with the
worker's markWorkflowGroupPickedUp (running). When the late queued stamp
landed second it overwrote running, and the cell looked stuck in queued for
the rest of the run. Skip the stamp when the same execution is already past
queued — the worker's authority wins.
* fix(table): per-page polling, optimistic skip on filled outputs, workflow column flag
- Polling now refetches only pages that contain in-flight cells instead of
every loaded page. Idle pages stay untouched while a cascade runs.
- run_column optimistic patch mirrors server eligibility on mode='incomplete':
cells with filled outputs no longer flip to queued only to revert seconds
later when the server returns 0 triggered.
- Hide the Workflow column type behind NEXT_PUBLIC_WORKFLOW_COLUMNS_ENABLED
(default false). Existing workflow groups keep rendering.
* fix(table): memoize infinite-rows queryKey so polling effect doesn't reset every tick
* fix lint
* fix(table): serialize polling ticks to prevent overlapping fetches
* fix(table): fire-and-forget run-column dispatch
Large fan-outs (thousands of rows) issue sequential trigger.dev HTTP calls
inside scheduleRunsForRows.batchEnqueue. Awaiting that loop held the HTTP
response (and the AI tool span) open for ~5 min on a 6k-row table — the user
saw an 11-min "running" because the tool didn't return until every job had
been enqueued. Run the dispatcher in the background and return immediately;
contract response now reports `triggered: null` since the count isn't known
synchronously.
* improvement(table): preserve row identity across poll refetches
Each poll tick brings back a fresh page from the server with all-new row
objects, even though most rows haven't changed. setQueryData was replacing
the whole page reference, which made every memoized <DataRow> in the page
re-render every 1.5s. Now we shallow-compare each fresh row against the
cached one and reuse the cached reference when nothing changed; only rows
whose data or exec status actually flipped re-render.
* fix(table): treat manual-bypass eligibility as runnable
Refactoring eligibility into classifyEligibility split the runnable answer
into two reasons: 'eligible' (deps satisfied) and 'manual-bypass' (autoRun=
false group on a manual run, deps don't apply). isGroupEligible only treated
'eligible' as runnable, so a manual "Run all rows" on a single autoRun=false
group filtered every row out and returned triggered: 0. Cells flashed
queued from the optimistic patch then went empty when the refetch landed.
Build times grew monotonically from ~200s to 1305s (~6x) after #4478 landed.
The Turbopack FS cache flag combined with sticky-disk persistence caused
unbounded cache growth. Reverting both — keeping the poweredByHeader and
optimizePackageImports trim from the original PR.
* fix(files): skip zip and return plain .md when no embedded images
* fix(files): use imageIds.length guard instead of assetMap.size
* fix(files): move imageIds.length guard before async fetch to avoid unnecessary work
* fix(revenuecat): align tools and block with REST v1 API spec
- Validated all 10 tools and the block against context7 REST v1 docs
- Unwrap {value:{subscriber}} envelope across post-receipts, attributes,
entitlements, and Google subscription endpoints
- Trim entitlement output to documented fields (expires_date,
grace_period_expires_date, product_identifier, purchase_date)
- Add subscriber output fields: last_seen, original_application_version,
other_purchases, subscriber_attributes
- create_purchase: productId optional (Google-only required); add
introductoryPrice, attributes, updated_at_ms; surface customer + subscriber;
X-Platform required; presentedOfferingIdentifier and paymentMode
- update_subscriber_attributes: read response and surface subscriber;
note required updated_at_ms
- defer_google_subscription: enforce XOR(extendByDays, expiryTimeMs) and
1-365 range; expiryTimeMs as one-of alternative
- grant_entitlement: duration optional, added endTimeMs (one-of)
- refund_google_subscription: corrected endpoint to
/transactions/{storeTransactionId}/refund
- delete_customer: read 'deleted' field (was 'was_deleted')
- list_offerings: corrected X-Platform values
- get_customer: count active subscriptions by expiry/refund
- Added shared throwIfRevenueCatError helper for {code, message} envelope
- Moved type coercions from tools.config.tool to tools.config.params to
preserve dynamic refs
* docs
* fix(revenuecat): address PR review feedback
- create_purchase: wrap JSON.parse(attributes) with try/catch and clear error
- update_subscriber_attributes: drop subscriber output (endpoint returns
empty body) and guard JSON.parse on attributes
- grant_entitlement: throw when both duration and endTimeMs are provided,
matching defer_google_subscription behavior
* fix(revenuecat): tighten docs after sub-segment validation
- delete_customer: drop dead was_deleted fallback (docs specify 'deleted')
- grant_entitlement: mark duration + startTimeMs as deprecated, clarify
startTimeMs only affects expiration calc (not grant time)
- list_offerings: replace vague platform description with documented
X-Platform enum (ios, android, amazon, stripe, roku, paddle)
* docs
* fix(revenuecat): clear duration default when endTimeMs is provided
The duration dropdown defaults to 'monthly' so any user filling in the
advanced endTimeMs field would otherwise hit the XOR guard. Clear
duration in the params mapper so endTimeMs takes precedence.
* fix(revenuecat): clear extendByDays default when expiryTimeMs is provided
Mirror the duration/endTimeMs fix from 8204e89: when expiryTimeMs is
populated, clear extendByDays in the params mapper so the empty-string
form value does not trip the XOR guard. Also harden the tool-level XOR
checks in defer_google_subscription and grant_entitlement to treat empty
strings as undefined for direct (non-block) callers.
* fix(revenuecat): guard NaN in time-ms mappers and require integer days
- Block params mapper: only clear duration/extendByDays when the parsed
endTimeMs/expiryTimeMs is finite, so invalid input does not silently
discard the user's valid companion default
- defer_google_subscription: validate extendByDays as integer (was
Number.isFinite), matching the error message
* fix(revenuecat): fall back to Date.now() when request_date is malformed
A malformed request_date would parse to NaN, making every entitlement
and subscription compare false and silently zero active counts. Fall
back to Date.now() when the parsed value is not finite.
* fix(revenuecat): unwrap value envelope in list_offerings response
* improvement(revenuecat): include updated_at_ms in attributes placeholder and wand prompt
* fix(logs): relax fileSchema so execution logs with files render again
* improvement(logs): align fileSchema with shared UserFile type
- contracts/logs.ts: replace local fileSchema with mediaUserFileSchema (the established UserFile boundary schema with .passthrough())
- file-download.tsx: drop local FileData interface, use UserFile from @/executor/types
* improvement(contracts): promote userFileSchema to primitives
Move the canonical UserFile boundary schema out of tools/media/shared.ts
(where it didn't belong — logs aren't media tools) into primitives.ts as
userFileSchema. Update logs, stt, and video contracts to import from the
shared primitive.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>