mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-01 14:59:19 +08:00
85a322667825e3ddd739ea18c5ba92e9cfb02b43
6387 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
85a3226678 |
fix(security): stop redirects replaying request bodies and leaking credentials (#6941)
* fix(security): stop redirects replaying request bodies and leaking credentials `secureFetchWithPinnedIP` passed its options straight into the redirect recursion, so a 301/302/303 replayed the original method and body — delivering a non-idempotent write twice — and forwarded `Authorization` and every other caller header to whatever origin the upstream named. `followRedirectsGuarded`, a hundred lines above it in the same file, already had the correct RFC 9110 rules. The two had drifted, and the drift is the bug. Both now route through one `resolveRedirectHop`: - 303, and 301/302 on POST, degrade to a bodyless GET and drop the entity headers that described the removed body. - A cross-origin hop drops every caller header, not just `Authorization`. - A cross-origin hop that would forward a body is refused. `stripAuthOnRedirect` still narrows same-origin hops for endpoints that redirect to a target carrying its own signed URL. Verified by stashing the fix and re-running: 4 of the 6 new tests fail against the old code. The 2 that pass either way cover same-origin behaviour that was already correct. * fix(api): preserve HTTP redirect compatibility |
||
|
|
58aa6379e0 |
feat(cli): add chat command (#6937)
* feat(cli): add chat command * fix(cli): harden chat command execution |
||
|
|
cd0516cade |
fix(billing): separate Enterprise reporting periods from Stripe terms (#6942)
* fix(billing): separate Enterprise reporting periods from Stripe terms * fix(billing): reconcile accepted legacy intents * fix(billing): keep accepted legacy intents fail-closed * fix(billing): reconcile accepted retired intents * fix(billing): retire invalid legacy intents |
||
|
|
6a45b0d4a6 |
feat(library): Best AI Agent Platforms for Connecting Your Existing Tools (#6946)
Co-authored-by: Sim Pi Agent <pi@sim.ai> |
||
|
|
4c41fc6c3e | fix(setup): bump sim-setup to 1.0.1 (#6944) | ||
|
|
6b7fd1a88d |
fix(webhooks): stop the generic webhook publishing a closed output schema (#6939)
* fix(webhooks): stop the generic webhook publishing a closed output schema
Declaring outputs on the generic webhook trigger did not add three reference
completions — it made those three the only legal fields on the block.
`collectBlockData` registers any non-empty output declaration as an exhaustive
schema, and `resolveBlockReference` then throws `InvalidFieldError` for any
reference outside it that resolves to `undefined`. A generic webhook receives
whatever the caller sends, so every workflow reading a body field started
failing the moment a delivery omitted that field, instead of resolving to
`undefined` and letting the condition evaluate falsy as it always had.
Revert the declaration to `{}` and record why it has to stay that way. The
request metadata is still merged into the workflow input by the provider's
`formatInput`; it is only undeclared, which is what keeps the shape open.
Offering these as editor completions needs a way to mark outputs as hints
rather than a closed schema — a change to `getRegistrySchema`, not to this list.
Pins the behavior at the executor level rather than on the trigger config,
since the config assertion is what passed while the block was broken.
* chore(audits): re-record the workspace module-graph baseline
`check:tool-registry-boundary` fails on CI for any branch right now: the
knowledge page measures 2255 modules against a 2209 baseline, one module past
the max(25, 2%) allowance. It passes locally at 2253, which is why it only
shows up in CI — the two platforms resolve a couple of modules differently, and
the route happened to sit inside that gap.
The drift is not from any one change. 26 of the 34 recorded routes have grown
since the baseline was last written, by up to +44. Measuring this branch's
route with and without its own diff gives 2253 either way, so it contributes
nothing; it is just the branch that happened to cross the line.
Re-records all 34 entries, which is what the script prescribes. No gateway was
added or removed on any route — only the module counts moved — so the boundary
this audit exists to protect is unchanged and the first assertion, that the
tool registry stays out of every workspace page graph, still passes.
Worth a separate look at why the workspace pages have grown this much; this
commit only stops a stale number from blocking unrelated work.
* chore(tests): give the block-data test helper an explicit return type
|
||
|
|
00d8a3fbfe |
fix(knowledge): refund a processing attempt whose dispatch never happened (#6938)
* fix(knowledge): refund a processing attempt whose dispatch never happened `markDocumentsQueued` spends one attempt from the processing budget on every dispatch, and `clearDocumentsQueued` already withdraws the queue stamp on the one path that proves nothing was dispatched — but it left the attempt spent. The budget exists to stop re-billing a document that keeps failing the same way in processing. An attempt that never reached a worker teaches it nothing, so an infrastructure outage — a Trigger.dev region error, an exhausted quota — burned the allowance without a single run. MAX_PROCESSING_ATTEMPTS such outages dead-letter the document, and the connector sweep filters on `processingAttempts < MAX_PROCESSING_ATTEMPTS`, so automatic recovery stops for a document that was never processed once. Refunded in the same guarded statement as the stamp, so it can only ever give back the charge this call made, and floored at zero. Also corrects the stale-lock TTL doc, which still described the reclaim as measuring `updatedAt` after it moved to `COALESCE(syncLockLeaseAt, updatedAt)`, and the attempt-budget rationale, which predates the refund. * chore(audits): re-record the route module-graph baseline Every workspace route had drifted past what the baseline records, uniformly: the shared `[workspaceId]/layout.tsx` grew +28 and each route inheriting it grew +27 to +29. The knowledge page carries +18 of its own on top, which put it +46 over a +44 allowance and was the only entry actually failing. Bisecting the growth across the last five commits on staging shows 2249 → 2251 → 2252 → 2253 → 2255 — one or two modules per unrelated feature PR, not a single regression dragging in a fat dependency. That is the organic creep the `max(25, 2%)` ratchet is meant to absorb and the re-record is meant to settle. Counts only: all 34 entries are preserved and every entry's gateway set is unchanged, so the registry-reachability gate and the per-entry ratchet keep exactly the strength they had. |
||
|
|
a0a14383f0 |
feat(enrichments): add LinkedIn profile lookup (#6926)
* feat(enrichments): add LinkedIn profile lookup * fix(enrichments): request Findymail profile data * fix(enrichments): avoid Findymail profile charge * refactor(enrichments): omit Findymail profile option |
||
|
|
e578cfe5dd |
feat(files): mship file writing improvements- #6933 (#6933)
feat(files): mship file writing improvements (#6933) |
||
|
|
29acfb10ff |
improvement(mship): mship file fixes (#6918)
* improvement(files): preserve editable page source on round trips * improvement(pages): harden previews and mobile rendering * fix(copilot): keep generated API keys accessible * fix(files): rewrite page sources after upload finalization * fix(pages): expose compile diagnostics through VFS * fix(copilot): surface classified tool access errors * fix(copilot): surface actionable server tool errors |
||
|
|
63569a2459 | fix(connectors): count hard-kill failures and cap deletion blast radius (#6909) | ||
|
|
01795e1ed2 |
fix(combobox): report every open, not just the dismissals Radix initiates (#6931)
The Combobox owns its `open` state but renders a controlled Radix `Popover`
(`PopoverAnchor` + `open={open}`, no `PopoverTrigger`), and the consumer's
`onOpenChange` hung off that Popover alone. A controlled popover reports only
transitions it initiates itself, so outside-click and Escape arrived and nothing
else did: the trigger, the chevron, focus, Enter/Space/ArrowDown, and
select-to-close were all the component's own `setOpen`, invisible from outside.
Every consumer refreshed on open or reset on close, so the damage stayed quiet —
the credential selectors, MCP tool selector, workspace-file picker, connector
modal, and the sub-block dropdown's remote option list simply never refreshed
when opened. Then #6881 gated the agent block's `toolGroups` on the same signal
to keep the group build off the canvas's hot path, and a picker that could not
learn it was open built nothing: the dropdown rendered "No tools found" over the
full block registry.
Every transition now goes through one `changeOpen`, which Radix's own
`onOpenChange` also feeds, so `setOpen` has exactly one caller and the callback
cannot be missed. It dedupes through a ref, because several paths both close and
let the popover dismiss — a redundancy the raw setState absorbed silently but a
consumer callback would not — and reading that ref lets the toggles resolve
their next value without re-creating their handlers on every open.
Tests cover the transitions Radix never reported (trigger click both ways,
keyboard open, Escape) and the consumer shape that made this visible: options
supplied only once the dropdown says it opened must render, not the empty state.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
8be58682ca |
fix(custom-blocks): give a custom block's logo a tile its header chip can paint (#6929)
A custom block with an uploaded logo declared bgColor 'transparent', which
meant "the image is the whole tile" — true on tile surfaces, where the image
fills the box, but wrong for the canvas node header. The header chip sets its
label beside the icon rather than under it, so an unpainted chip left the label
nothing to contrast: perceivedBrightness('transparent') is null, the foreground
fell back to white, and the block name rendered white on the white card. The
header showed a bare logo where every other block shows a chip.
Custom blocks with an image now wear the same white plate as every other
light-tiled provider, so they read as an ordinary integration everywhere.
Icon and tile resolve together from one precedence rule, so a block can never
paint a tile its icon disagrees with.
|
||
|
|
fd5ea3e399 | fix(billing): cast outbox payloads for JSONB operators (#6928) | ||
|
|
ab66ce9149 |
fix(admin): harden dashboard billing operations (#6914)
* feat(admin): make dashboard billing operations durable * fix(admin): close dashboard recovery gaps * fix(admin): preserve member operation lock order * fix(admin): harden durable operation boundaries * fix(admin): report member operation failures accurately * test(invitations): cover locked seat admission * feat(enterprise): gate owner activation on acceptance * fix(enterprise): keep owner activation recoverable |
||
|
|
dbbe99e473 |
fix(integrations): validation pass over Crunchbase, PitchBook, and CB Insights (#6925)
* fix(crunchbase): widen tier-gated collection allowlists and cap the deleted feed The deleted-entity, autocomplete, and fields-metadata allowlists each held only the collections the narrowest package tier publishes, so requests valid on a richer package were rejected locally before any request went out. An Advanced Financials key could not read the funding-round deletion feed at all. - deleted-entity collections: 9 -> the 14-collection union across all tiers - autocomplete and fields-metadata: 14 -> all 43 collections - clamp the deleted feed to its documented max of 25, not Search's 1000 - offer "All collections" so the cross-collection feed stays reachable - name the richer-tier card additions instead of presenting the base set as exhaustive Also rewrites a test that asserted the broken behavior and tightens a substring URL assertion that passed on the value it was meant to reject. * fix(pitchbook): stop a rejected API key reaching block output and logs PitchBook's 401 body echoes the submitted key back inside `message`. No PitchBook tool declared an `errorExtractor`, so the failure fell through to the generic chain, whose first entry returns `data.message` verbatim — putting the credential in the block error, the run log, and any agent context reading the failure. The existing scrubber sat in `transformResponse`, which never runs on a non-ok response. - add a `pitchbook-errors` extractor that replaces the unauthorized message with a fixed string, and wire it through all 91 tools - the extractor returns undefined unless the body carries a `message`, so a foreign 401 on the shared fallback chain is never labelled a PitchBook failure - correct `investor_preferences.preferredIndustry` to the shape the API returns - make `company_industries.emergingSpaces` opaque; its item shape is undocumented - reject a non-list of article ids instead of throwing a bare TypeError * fix(cbinsights): reject malformed input instead of silently rescoping a billed query CB Insights is metered, so a filter that fails to parse must fail the request — dropping it does not narrow the result, it charges for a query the caller never asked for. - reject an unrecognized boolean rather than dropping it, which had been widening a VC-backed firmographics search - reject a non-numeric limit instead of falling back to the endpoint default - reject non-text filter entries instead of stringifying them to "[object Object]" - accept only asc/desc for sort direction; a typo had returned the bottom of a metered result set as though it were the top - treat a whitespace-only numeric bound as unset, not as zero - drop `totalHits`/`totalHitsRelation` from list business relationships; that endpoint reports no total, so both were permanently null - trim `nextPageToken`, matching the id fields Also moves the token cache onto `lru-cache` per the in-process caching rule, replacing hand-rolled TTL arithmetic and a manual prune. * chore(harmonic): drop the team-key help text from the credential descriptor * chore(tools): regenerate tool metadata for the validation fixes * fix(tools): redact the retained error body, not just the message Scrubbing the extracted message left the raw provider body reachable: `createTransformedErrorFromErrorInfo` attaches `errorInfo.data` to the thrown error and the executor surfaces it on the failed tool's `output.data`, so a PitchBook key rejected with an echoing 401 still reached block output and agent tool results via `output.data.message`. - add an optional `redactData` to the error-extractor contract, so an extractor that exists because a provider echoes a credential can replace the body too - retain `redactErrorData(errorInfo, extractorId)` in place of the raw body - PitchBook replaces only the unauthorized body; every other failure is untouched - cover the executor path itself, since asserting on the redactor directly still passes when nothing is wired to it |
||
|
|
aca152c7fb |
fix(env): put runtime config on <html> so client reads can't outrun it (#6923)
* fix(env): put runtime config on <html> so client reads can't outrun it The inline script that assigns `window.__ENV` is rendered from the component tree, so it lands ~13KB after the `<script async>` bootstrap tags React emits in the preamble. `appBootstrap` calls `hydrate()` synchronously whenever `self.__next_s` is empty — which it always is now that the script is a plain tag rather than a `beforeInteractive` one, that queue having been the only thing sequencing the assignment ahead of hydration. So module bodies and the first commit could both read env before the assignment landed: the socket URL fell back to the page origin for the life of the document, `getBaseUrl()` threw, and every module-scope flag in `env-flags` froze on nothing. Carry the same snapshot on `<html>`, the document's first tag, and read it in `getEnv` when `window.__ENV` is not yet assigned. Parsing is memoized against the raw attribute rather than against having run once, so the cache can never serve a value the document no longer carries. `window.__ENV` stays the public global and the preferred read, and both transports are built from one function so they cannot drift. Alongside: guard the read-only webhook-URL field so a base URL it cannot resolve is a blank field rather than a dead canvas; report what the workflow error boundary catches, which it previously swallowed entirely; and enable PostHog's native exception capture, since error boundaries only ever see their own subtree and chunk-load failures, rejected promises and throws from event or socket callbacks reached nothing. * fix(realtime): count each failed connect attempt once `manager.reconnect()` calls `open()`, whose error path emits `error` — which the socket re-emits as `connect_error` — and then emits `reconnect_error` itself. A failed reconnect therefore reached both handlers and advanced the counter twice, so the outage report tripped on the second real attempt while claiming three. Count in `connect_error` alone: it is the only handler that fires exactly once for both the initial failure and every retry. `reconnect_error` keeps its log line and states why it deliberately does not count. * fix(workflow): let an unresolvable webhook URL fail loudly Reverts the guard added earlier in this PR. A blank row titled "Webhook URL" is a worse outcome than a crash: it explains nothing, and the value is one a user copies into a third-party provider, so any substitute — a guessed page origin, an empty string — is a URL that provider accepts and then never delivers to. The read can no longer come back empty from the hydration race this PR fixes, so reaching it at all means the deployment has no application base URL, which breaks webhook registration and callbacks regardless. The error boundary now reports what it caught, so the throw names its own cause instead of surfacing as an unexplained fallback. |
||
|
|
4ad1d53de9 | fix(copilot): sanitize edit_workflow result state before returning it to the agent (#6904) | ||
|
|
f9eaadfc70 |
fix(knowledge): give failed documents a grace while their retries are still scheduled (#6924)
The sweep treats failed as terminal — "the run that produced it has ended" — and that was true when processing ran inline. Since dispatch became asynchronous the worker writes failed and then rethrows, and the processing task retries up to three times, so failed is a resting state between attempts rather than a final one. The sweep gave it no grace at all, so it reclaimed a document mid-retry chain, deleted its embeddings, and re-dispatched with a fresh pass id: two live runs, two indexing passes, two bills, through the one state deliberately left unguarded. Aged from processingCompletedAt, which every failure write stamps, so it reads exactly when the last attempt ended rather than when the document was dispatched. Sized at the queue grace rather than a run-duration bound. What is being waited on between attempts is a re-queue behind the same global concurrency limit, not another run, so on a large backlog the next attempt starts hours after the previous one ended and a run-duration bound would still reclaim live work. The grace itself is now an expression rather than a constant — corpus over queue concurrency, times occupancy, times a contention factor — with each input documented where it can be re-measured. The previous 240 was derived from a 2,600-document corpus and sat below the drain time of the 7,730-document connector it was written for. Rounding is up: too small silently re-bills live work, too large only delays recovery of documents nothing is processing. Also withdraws the queue stamp when every dispatch in a batch fails, so a provably-undispatched document keeps next-sync recovery instead of waiting out a grace it never earned. Scoped to the batch's own ids, to rows still pending, and by compare-and-set on the exact stamp that call wrote, so a concurrent batch that re-stamped a document keeps its grace. Best-effort by design, while the stamp write still throws: a failed stamp means the grace cannot be promised and dispatching anyway is the unsafe direction, whereas a failed withdrawal only delays recovery and must not mask the dispatch error underneath it. |
||
|
|
1ced0b6065 |
fix(knowledge): stop the stuck-document sweep reclaiming still-queued documents (#6921)
* fix(knowledge): stop the stuck-document sweep reclaiming still-queued documents The sweep gave 'processing' a staleness cutoff but gave 'pending' and 'failed' none, and the only cross-run guard was uploadedAt < syncStartedAt, which scopes within a sync but not across them. That was harmless while document processing awaited inline: documents were terminal by the time a sync ended. Since dispatch became asynchronous they sit 'pending' until a worker picks them up, so the next sync deleted their embeddings, reset them, and re-dispatched while the original runs were still executing — and each re-dispatch mints a fresh pass id, so it billed again. Queued documents now get a grace period derived from queue drain time rather than run duration: the processing queue's concurrency is 20 and global across workspaces, so a 2,600-document corpus takes roughly two hours to drain. The existing 45-minute threshold bounds a run, not a wait, and would still reclaim live documents on any corpus over about 900. No column records dispatch time, so the signal is processingStartedAt falling back to uploadedAt. The sweep now stamps that column when it re-dispatches, and the user-triggered retry stamps it instead of clearing it — without both, only a document's first dispatch was protected and the sweep churned once per sync forever on anything that kept waiting. The grace narrows the duplicate-billing window but cannot close it: a re-dispatch mints a new request id and the Trigger idempotency key is scoped per dispatch by design. Closing it durably needs a document-scoped key or a dispatch-generation column, recorded in TSDoc and deliberately not built here. * fix(knowledge): record dispatch time in its own column instead of overloading the start time Two review findings on this PR traced to the same root: the queue grace was reading processingStartedAt, a column that means something else. Externally, a pending row carrying a dispatch timestamp reported a processing start time for work that had not started. Internally, updateDocument sets a document pending and refreshes uploadedAt while leaving the previous run's processingStartedAt in place, and completion never clears it — so the sweep aged a re-dispatched document from a leftover stamp rather than its actual dispatch, and could reclaim it inside the grace window while the earlier queue entry was still live. That reopens the duplicate-billing race this PR closes. processing_queued_at is written on every re-dispatch and read by the sweep; processingStartedAt goes back to meaning what its name says, so its external contract is byte-identical to before this PR. Not stamped on first dispatch: the row is created and dispatched inside the same sync run, so uploadedAt is already accurate there and a guarded write per upload would buy no behavior. The internal document route returns a full table row through a passthrough schema, so the new column would have shipped as an undeclared raw Date. Declared and serialized explicitly instead. * fix(knowledge): stamp the queue time in the dispatch funnel, not at each call site Giving dispatch its own column was not enough. The connector content-update path updates a row in place with pending status and a fresh uploadedAt while leaving every prior-run processing column intact, so a document dispatched once already carried a stale queue stamp, the refreshed uploadedAt was never consulted, and the row aged from a dead run's timestamp — the same bug one level down. "Written on dispatch and only on dispatch" is now structural rather than a convention three call sites remember: the stamp lives in processDocumentsWithQueue, which every one of the nine dispatch paths already goes through, so none can forget it. The sweep's and the retry's own stamps are kept because they land inside the same transaction as their reset, so a document keeps its grace even if the dispatch that follows throws. The funnel also clears processingStartedAt, guarded on the row still being pending so it cannot disturb a worker's compare-and-set. That closes the API-facing half at its source: a pending row can no longer report a start time from any path, including the content-update path no serializer would have seen. |
||
|
|
2074afe8ed |
feat(library): Best AI Automation Tools in 2026 (#6922)
Co-authored-by: Sim Pi Agent <pi@sim.ai> |
||
|
|
9a66fbb774 |
fix(auth): bound the three unbounded session-policy caches (#6919)
* fix(auth): bound the three unbounded session-policy caches security-policy.ts and session-policy.ts are read from Better Auth's session create and update hooks, so they run on every session validation. All three caches were plain Maps with a hand-rolled 'Date.now() - fetchedAt < TTL' read and no ceiling: entries were released only by an explicit invalidate, so they grew for the life of the process. membershipCache is the sharpest of the three because it is keyed by user, not organization — one entry per user who ever authenticated on that instance. Move all three to LRUCache, already a direct dependency and the pattern copilot/entitlements.ts and providers/client-cache.ts use. The library owns the TTL and the ceiling; every existing invalidate* keeps working unchanged. Two things to preserve, both now pinned by tests: - membership results keep their asymmetric TTL (a non-member result expires far sooner, so a user who joins through a path this codebase never sees cannot dodge the new org's policy). Expressed as membershipCacheTtlMs rather than an inline ternary, since it is a security property and not a tuning knob. - reads test '!== undefined', because a version is a number and a membership is nullable — a truthiness check would treat both as a miss. security-policy.ts had no test file; adds one covering caching, invalidation, failure fallbacks and the TTL asymmetry. * fix(auth): raise the cache ceilings to a memory backstop getSessionCookieCacheVersion feeds Better Auth's session.cookieCache.version, so these are read on every session read, not just create/refresh. At max: 1000 a busy instance could exceed the live key set inside the 60s TTL and start evicting early — never a wrong answer (a miss is one indexed lookup, exactly the pre-cache behaviour) but a hit-rate cliff on a hot path. Entries are a few dozen bytes, so headroom is nearly free: orgs 1k -> 20k, users 10k -> 100k. That is single-digit MB at worst and puts the ceiling far above any plausible per-instance working set, leaving it as the memory backstop it was meant to be. * docs(rules): write down the caching decision tree The lifecycle-map-vs-TTL-cache distinction, the ceiling-as-backstop sizing, and the fetchMethod-unless-you-need-a-hang-deadline call each took real digging to settle. Recording them so the next cache does not re-derive the same answers — or re-introduce the unbounded tenant-keyed Map this branch just removed. Notes the two non-obvious traps behind that: ttl alone does not bound memory without a ceiling, and React cache() is a no-op in Trigger workers, so a gate that looks free on a settings page is uncached and per-block on the executor. |
||
|
|
b125cfdc05 |
fix(knowledge): dispatch document processing from inside Trigger.dev runs (#6920)
isTriggerAvailable() returns false inside Trigger.dev worker runs, so every connector document has been downloaded, parsed, chunked and embedded inside the sync task at concurrency 5 rather than dispatched to knowledge-process-document. The per-document fan-out is inert in production. Confirmed from a crashed run's spans: the dispatch log line carries backend: "direct", and no knowledge-process-document runs exist for that knowledge base while other knowledge bases dispatched normally in the same window. On the largest connector — 7,730 documents, individual XLSX files over 20MB — that exhausts the heap and OOMs the sync. Inside a run the answer is unconditionally yes: the platform is what is executing the process, so no environment guess can beat the run marker. Two independent signals, because this has now been got wrong twice — the SDK's ambient taskContext.isInsideTask, already load-bearing in getAsyncBackendType for the same carve-out, and a marker written by the global init lifecycle hook, which uses only the documented public surface. Neither reads TRIGGER_SECRET_KEY or TRIGGER_DEV_ENABLED. Resolving true inside a run is safe under either hypothesis about which conjunct was false: if a run process lacks the secret key the SDK rejects the batch trigger and dispatch falls back in-process, which is where a false predicate lands today. The first evaluation per process now logs the resolved inputs, naming which conjunct is false. A predicate unit test would not have caught either shipment; both were deploy-environment differences only observable from inside a worker. |
||
|
|
d8d983859a |
fix(harmonic): correct the destructive-clear copy and stop double-billing enrichment (#6915)
* fix(harmonic): correct the destructive-clear copy and stop double-billing enrichment Follow-up to #6902, from a final validation pass against Harmonic's OpenAPI and API reference. No endpoint, method, or response mapping changed. - The `personUrns` field told users and the LLM that Clear Net-New Results "clears everything when omitted". That is the raw provider behavior the clearScope guard was added to block; omitting it now throws. The field is shared across three operations, so the wrong sentence was being served as guidance on all of them. - Bulk email enrichment deduplicated LinkedIn URLs before canonicalising them, so `.../in/foo?utm_source=x` and `.../in/foo` were submitted as two people. Harmonic bills per submitted entry, so this spent quota twice and double-counted against the 5,000 cap. Deduplicate after canonicalising. - The two documented bulk-enrichment failures carry a code in `error` and no message anywhere, so quota exhaustion surfaced as "Request failed with status 429". Render the code with its counters instead. Gated on those counters being present: `extractErrorMessage` without an explicit id walks every extractor in order, and claiming a bare `error` key swallowed OAuth's `error_description`. - An enrichment 404 whose detail carries only the URN no longer discards it. - Report the identifier conflict before complaining about an individual URL. - Validate `companyContextUrns` as company URNs, like every other URN param. Forward-compat: `user_saved_search_type` is passed through rather than checked against a fixed set. It is display metadata nothing branches on, and Harmonic owns the enum — an allow-list turned any value they add into a hard failure of the whole list while the selector reading the same rows kept working. Also drops `USER_CONNECTION`, which Harmonic documents as unsupported via the API, removes three superseded types and one dead helper, and extends the "credential never reaches a URL or body" assertion from 4 tools to all 13. * fix(harmonic): fold equivalent profile URLs and stop blank entries failing a batch Review round on #6915. - Blank and non-string `personLinkedinUrls` entries are dropped before the mutual-exclusivity check. Moving the filter after per-URL validation meant a list like `['']` alongside person URNs reported "not both" — naming a conflict the caller never created — or failed the URL parse instead of reading as absent. - Deduplicate on a profile key rather than the canonical string, so `linkedin.com/in/x`, `www.linkedin.com/in/x` and a trailing slash count once. Harmonic canonicalizes and silently deduplicates server-side and reserves quota afterwards, so this does not change what is billed; it keeps Sim's own 1-5000 accounting in step with the set Harmonic accepts, so a batch of equivalent URLs is not rejected locally for a cap it never reaches. Regional subdomains stay distinct: folding `uk.linkedin.com` into `www.` would assert an equivalence Harmonic does not document, and the URL kept for display must remain the one the caller supplied. * fix(harmonic): fold only recognized profile URLs, never pass-through ones Review round on #6915. The profile key added last round was applied to every entry, but it is built from host and path alone. A URL forwarded verbatim for Harmonic to adjudicate keeps its query, port and fragment significant, so two distinct identifiers collapsed to one key and the later one was dropped before Harmonic ever saw it. The key now applies only to a URL `normalizeLinkedinProfileUrl` already canonicalized — where the query and fragment are gone by construction, so folding host and trailing slash is safe. Anything passed through deduplicates on its exact text. |
||
|
|
5b28da1989 |
fix(tables): accept plain row query predicates (#6916)
* fix(tables): accept plain row query predicates * fix(cli): show table predicate group syntax |
||
|
|
8eebd6e687 |
fix(enrichments): project provider failures (#6917)
* fix(enrichments): project provider failures * fix(enrichments): share Prospeo failure projection |
||
|
|
42f6287911 |
feat(byok): add organization-wide key inheritance (#6834)
* feat(byok): add organization key management * feat(byok): inherit organization keys at runtime * feat(byok): add organization scope to BYOK settings * fix(byok): refresh org key state after mutations * fix(byok): hide stale inherited status badges * chore(db): drop colliding byok migration ahead of staging merge Staging independently claimed 0293. Remove ours so the merge is clean; it is regenerated at the next free index right after. * chore(db): regenerate byok migration at 0296 Staging claimed 0293-0295 during the merge; the regenerated SQL is byte-identical to the dropped 0293. * docs(byok): document organization scope, precedence, and the full provider list The BYOK section described workspace-scoped keys only. Add the organization scope, its Enterprise requirement, the per-provider precedence rule, what an entitlement lapse does, and the Pi sandbox exposure. Refresh the provider table from the settings page, which had drifted from 14 to 34 entries. * feat(byok): open organization keys to every organization plan Organization BYOK was gated on Enterprise, but an organization is the only thing that can hold the keys, so every plan that can own an organization should qualify — Pro for Teams, Max for Teams, and Enterprise. Add checkOrgPlan/resolveOrganizationPlan beside the Enterprise pair rather than widening checkEnterprisePlan, so the Enterprise-only gates (Access Control, whitelabeling) are untouched, and restore resolveOrganizationEnterprisePlan to module-private now that BYOK no longer needs it. * perf(byok): cache the organization entitlement, not the key material getBYOKKey runs once per agent block and once per hosted-capable tool call, so a loop over N items resolved N times — and each organization-inheriting resolution paid three sequential billing queries on top of the two key reads. Split the two reads by staleness tolerance. Key rows stay fresh, because revocation must be immediate. The entitlement is a billing gate that tolerates bounded staleness in the harmless direction (a lapsed organization keeps using its own key for <=60s), so cache it per organization with an in-flight share so concurrent blocks issue one query set. The management surfaces keep reading it fresh, so an organization that just upgraded is never told otherwise. Also run the block check and subscription read in parallel inside resolveOrganizationPlan, and carry the resolved scope on BYOKKeyResult so a log line can say whether a run used the workspace's key or an inherited one. * feat(byok): let workspaces store the Z.ai and Cohere keys the runtime reads Both ids were already in the BYOK contract enum and both are resolved at execution time — getApiKeyWithBYOK reaches 'zai' (GLM models are in the hosted catalog, so the BYOK branch runs), and 'cohere' backs both the Embeddings block and Knowledge Base reranking — but neither appeared in the settings list, so there was no way to store the key either path looks for. Cohere had no icon; add one from the official multi-color mark so it stays legible on a light and a dark page. Cohere's embed-v4.0 is kbEligible:false, so the description says 'Embeddings and Knowledge Base reranking' rather than claiming KB embeddings. * improvement(byok): shorten the workspace scope chip to 'Workspace' It sits beside 'Organization', so the scope reads from the pair; 'This' only added width. * fix(byok): do not cache a billing outage as an unentitled organization resolveOrganizationPlan maps a failed billing read to false, which is indistinguishable from a real plan lapse. The entitlement cache stored that, so one transient outage held the gate shut for the full TTL and every inheriting run silently fell back to a metered hosted key — and the cache's rejection path, which exists to prevent exactly this, was unreachable. Give the resolver the onError option its neighbours already have and let the cached read ask for 'throw', so a failure stays out of the cache and the next resolution retries. Behavior for the call that saw the error is unchanged: getBYOKKey still fails closed. Reported by Cursor Bugbot. * fix(byok): propagate the subscription read's failure too The previous commit threaded onError through resolveOrganizationPlan's own catch, but getOrganizationSubscriptionUsable soft-fails to null on its own, so a failed subscription read still arrived as an ordinary 'no usable subscription' and returned a successful false — which the entitlement cache then stored for the full TTL. Thread the option into that call as well. Test it at the billing layer rather than the cache layer: the entitlement test mocks resolveOrganizationPlan wholesale, so it could never have caught this. Verified the new test fails against the previous commit. Reported by Cursor Bugbot. * refactor(byok): cache the entitlement with LRUCache, like copilot entitlements The hand-rolled version reinvented three things the codebase already has a canonical answer for. lru-cache is a declared dependency of apps/sim and lib/copilot/entitlements.ts already caches an entitlement with it — by storing the in-flight Promise, which is what makes concurrent callers collapse onto one resolution with no in-flight bookkeeping at all. TTL and the size bound come from the library. That removes the second Map, the manual eviction (and its interaction with an in-flight entry), and the dead value-while-refreshing state: 23 executable lines. The one thing the library does not cover is dropping a rejected promise so a billing outage is not cached for the TTL, which is kept and pinned by a test that fails without it. TTL expiry is no longer re-tested — that is the library's behavior, not ours, and lru-cache reads its clock at module load so faking timers never moved it. * refactor(byok): coalesce the entitlement read with the shared singleflight lib/concurrency/singleflight.ts is the codebase's coalescing primitive and oauth/credential-service.ts already pairs it with a read-through cache. Adopting that shape fixes a case caching the promise directly did not: a *hung* billing read wedged every caller for the full 60s TTL, where coalesceLocally evicts and rejects at its settle deadline. It also removes the hand-rolled rejection eviction — the cache is written only on the success path, so an outage leaves no entry by construction. The cache now holds booleans, which introduces the one trap worth a test: a truthiness check would read a cached false as a miss and re-query billing on every resolution for lapsed organizations. Pinned. * fix(byok): keep an abandoned entitlement producer from writing the cache coalesceLocally does not cancel a producer it timed out — its docstring says so explicitly — so writing the cache from inside the producer let a late billing result overwrite a fresher answer a retry had already cached, and hold it for a full TTL. Move the write onto the value the caller actually received. A caller that timed out throws before reaching it, so an abandoned producer now resolves into nothing. The test reproduces the overwrite and fails against the previous shape. Reported by Cursor Bugbot. --------- Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local> Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai> |
||
|
|
e4a1fbeaae |
fix(og): put the landing model and integration cards on the brandbook template (#6913)
* fix(og): put the landing model and integration cards on the brandbook template The five models/integrations cards were the last ones still rendering the retired dark card with the green square-icon logo. They now share the cover renderer the library, docs, and shared-file cards use, which retires og-utils and the assets only it referenced. Captions wrap to two lines so a catalog description survives the move; the eyebrow, pill row, and domain label have no slot in the reference template and are dropped, with the counts they carried folded into the two index captions instead. * fix(og): drop the dead hard-space helper and restore the unknown-context copy withHardSpaces lost its last caller when captions moved onto wrapLines, which packs the non-breaking space inline; its rationale moves to wrapLines with it. A missing context window rendered as a bare "context window", reading as a field label mid-sentence. formatTokenCount already returns "Unknown" for a null value, so the ternary was both redundant and wrong. |
||
|
|
040f40cd61 |
fix(connectors): stop rendering running and crashed syncs as successes (#6910)
* fix(connectors): stop rendering running and crashed syncs as successes The sync-history row derived its state from status literals the engine never writes — 'running', 'syncing', 'error' are connector statuses, not sync-log statuses. The in-progress branch was therefore false for every row that will ever exist, so a live sync rendered as a green success tick for its whole duration and a killed run left a permanent fake success reading "No changes". - Derive the row state from the three statuses the engine actually writes - Narrow the sync-log status contract from z.string() to an enum, and make the render switch exhaustive, so producer drift fails response validation and consumer drift fails the build - Add an interrupted state for a started row older than the stale-lock TTL: it is a crashed run, not a live one, and rendering it as a permanent spinner would trade one false signal for another * docs(connectors): record why the stale TTL is a hard ceiling for the interrupted state |
||
|
|
451d2ccbde |
fix(knowledge): stop billing a document once per processing attempt (#6911)
The usage sourceReference carried a per-attempt timestamp, which defeated by construction the eventKey deduplication it flows into. With three Trigger attempts plus the stuck-document sweep, one document could be billed four times for a single indexing pass. Key on the dispatch request id instead — already the codebase's name for one indexing pass, fixed across attempts of a run and fresh on every new dispatch — so retries collapse while a genuine re-index still bills. Falls back to the embedding pricing id where no pass id is threaded. usage_log has no retention, so keying on documentId alone would have suppressed legitimate re-indexing permanently. Thread requestId through the Trigger worker, which previously dropped it before the call — the path that actually retries. |
||
|
|
ea70f8dcf1 |
feat(harmonic): add contact workflow integration (#6902)
* feat(harmonic): add contact workflow integration * fix(harmonic): sync docs manifest * fix(harmonic): address integration review findings * feat(harmonic): add the missing people endpoints and fix two error paths Extends the integration from 4 to 13 tools, covering every non-deprecated people-scoped Harmonic endpoint, and repairs two defects found by validating the existing tools against Harmonic's OpenAPI and API reference. New tools: - Enrich Person (POST /persons) — the only path from a LinkedIn URL or email a workflow already holds to a Harmonic contact. - Get Person, Get Company Employees — account-based sourcing; employees returns URNs that chain into Batch Get People. - Saved-search net-new results and their acknowledgement, so a monitor stops reprocessing the entire result set on every poll. - Bulk email enrichment: submit, poll, and quota, plus Get Enrichment Status. Fixes: - The error extractor dropped Harmonic's string and object `detail` envelopes. A tool that names an extractor gets no fallback chain, so every FastAPI abort surfaced as "Request failed with status 403". The enrichment 404 also carries the scheduled `enrichment_urn`, which was being discarded — that URN is the only handle on the job, so it is now kept in the message. - The saved-search selector failed the whole dropdown instead of degrading: the response cap was half the sibling value on an endpoint that is unpaginated and returns every saved search with its full query object, and the option ceiling threw rather than truncating. Raised to 1MB and switched to truncate-and-warn, matching the other data-driven selectors. Clearing net-new results now requires an explicit scope. Harmonic treats an absent `entity_urns` as "clear everything", so an empty field would have silently discarded the backlog. Scope deliberately excludes company-side, deal, typeahead, network, and Scout streaming endpoints, and every endpoint retiring on 2026-11-05. --------- Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local> Co-authored-by: Waleed Latif <walif6@gmail.com> |
||
|
|
893e729b3f |
fix(og): put the shared-file card on the brandbook cover template (#6907)
* fix(og): put the shared-file card on the brandbook cover template * fix(og): bound the cover title and caption to the fixed canvas * fix(og): measure cover text with the font's real advance widths An average glyph width under-measures caps-heavy names and over-measures narrow ones, so a viewer-supplied file name could still clip off the fixed canvas. Measure against the same font Satori is handed instead, matching the library cover generator; the tests parse the font independently so the assertion is not made with the estimator it is checking. * fix(og): apply the leading compensation to the title, not the whole footer On the footer the 14px nudge dragged the caption into the bottom padding as well, and the caption has no phantom leading to correct for. Matches how the sibling cover renderers scope the same offset. |
||
|
|
865f8173ab |
feat(affinity): add Affinity CRM integration (#6908)
* feat(affinity): add Affinity CRM integration Adds the Affinity v2 API as a block with 70 tools, covering 86 of the 87 documented endpoints. Only Send Feedback is omitted — it reports product feedback to Affinity rather than doing workflow work. Endpoint families that differ only by an entity segment are one tool with an entityType param, so companies/persons field, list, row, and relationship reads, the company/person merge endpoints, and entity notes each collapse into a single operation. * chore(affinity): regenerate the docs manifest for the new integration page |
||
|
|
a99f61bee9 |
feat(api): add v2 resource management endpoints (#6900)
* feat(api): add v2 resource management endpoints * fix(cli): gate destructive v2 commands * fix(test): update v2 request-slice count * feat(cli): add shared workspace profiles |
||
|
|
85290693c4 |
refactor(search): one definition of what a search occurrence is (#6905)
Follow-up to #6901, closing two places where the workflow search index and the Note card that mirrors it could drift apart. Neither is a live bug; both are the shape that produced one — the card silently disagreeing with the panel about which hit is which, counted in one place and painted in another. THE SCAN. #6901 shared `foldSearchWhitespace` but left the scan around it duplicated: normalize, then non-overlapping `indexOf` stepping by `max(len, 1)`, written out once in the indexer and once in the renderer package. They agree today. They would stop agreeing the moment either grew whole-word matching, diacritic folding, or a regex mode, and the failure is silent. Both now call one `forEachSearchOccurrence` in `@sim/utils/string` — the only place either package can share, since the card renders from a package that cannot import from `apps/*`. THE DECLARATION. The indexer projects markdown escapes only for a field declaring `searchTextFormat: 'markdown'`; the card projects unconditionally, because it cannot read the block registry. Dropping that one line from the Note config would leave them disagreeing with nothing to catch it, so a test now pins it and explains why. Net negative in lines: this deletes a duplicated loop rather than adding a layer. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2e111f615c |
fix(search): answer a Note match on the canvas card (#6901)
* fix(search): answer a Note match on the canvas card A workflow search match inside a Note counted towards the result total and then highlighted nowhere: the editor panel renders nothing for a Note, and `clearCurrentBlock` — which the panel calls to refuse one — also cleared the shared `activeSearchTarget`, destroying the very target the card was about to paint. Searching a 15k-character note reported "1 of 6" and moved nothing. The card's read view is now the surface that answers: - A rehype plugin marks every rendered occurrence; the current one is picked by an ordinal counted with the same scan the indexer uses, and travels by context so cycling matches does not re-parse the document. - `<Streamdown>` is keyed on the query. Its memo comparator ignores `rehypePlugins`/`components`, so a plugin change alone cannot re-render it — marks appeared only when something else remounted the card, and then outlived the query that produced them. - The canvas selects, centres and expands the Note, because a compact card resets its scroll region to the top and cannot hold a position deep in its own body. Scrolling to the mark is `scrollTop` arithmetic, never `scrollIntoView`, which would drag ReactFlow's transformed viewport off-frame. - Title matches mark the name too. `activeSearchTarget` is re-published with a fresh identity on most of the search panel's renders, so subscribers take primitives. Holding the object in `WorkflowContent` — the panel's own ancestor — closed an unbounded update loop. Separately, the serializer escaped every underscore, writing `SB\_ACTION\_ROUTER\_SECRET` into the document. CommonMark's intraword rule means that backslash carries no meaning, and search matches the stored markdown, so it made anything with an underscore unfindable in a note that plainly showed it. Dropped outside code regions, where the serializer emits verbatim and a backslash is the author's own character. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(search): honour fences and folded whitespace in Note highlighting Two review findings, both real. The intraword-underscore cleanup guarded code with a pattern that recognised only the shortest delimiter forms — a bare ``` pair and a single-backtick span. A ````-fenced block, a tilde fence, or a ``multi-backtick`` span ended the region early and handed the rest of the author's code to the rewrite, turning `a\_b` into `a_b` inside their code sample. Fenced blocks are now walked a line at a time, tracking the opening delimiter exactly the way stripEmptyListItemLines already does (three or more, closed only by a run at least as long), and the inline branch matches a backtick RUN closed by one of equal length. The note scanner claimed to be the same scan as the indexer's `findTextRanges` but did not fold whitespace, which staging added since this branch was written. The indexer folds every `\s` to a space, so a phrase matches across a soft line break — which `remark-breaks` renders as a `<br>`, splitting the phrase over two text nodes that a per-node scan could never see. The hit counted in the panel and highlighted nowhere, the exact bug this branch exists to fix. The plugin now scans runs of continuously-readable text rather than single nodes, so a match spanning an inline boundary (a soft break, a bold word) is wrapped as several marks sharing one ordinal. Runs end at any non-inline element, so two paragraphs are never joined into a phrase the reader cannot see. `foldSearchWhitespace` moved to `@sim/utils/string`: the canvas card renders from a package, which cannot import from `apps/*`, and two copies of that rule silently disagreeing is precisely what produced the second finding. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(markdown): close a fence only on a bare delimiter run A closing fence carries nothing but its delimiter run; a line that merely starts with one is content. The guard matched the prefix alone, so an interior line like ` ````example ` inside a same-length fence ended the block, and every cleanup below then processed the author's remaining code as prose — dropping the backslashes from their `a\_b`. Both fence walks in this file shared that flaw, so both now go through one `closesFence`, which requires the run to be followed by nothing but whitespace. Strictly more conservative: a fence stays open longer, so more content is left verbatim. Scope, stated plainly: the serializer always opens a block with one more delimiter than the longest run inside it, so its own output cannot reach this shape today, and `postProcessSerializedMarkdown` only ever sees serializer output. This is a correctness fix that removes an unstated coupling to that choice, not a live corruption path. The tests therefore exercise `postProcessSerializedMarkdown` directly — a round-trip test of the same input would pass either way, which is exactly the vacuous check worth avoiding. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(markdown): skip quoted fences, and stop joining runs across inline tags Two review findings, both real, both the same shape: a rule that looked at the rendered form and forgot what the source actually says. QUOTED FENCES. The fence walk only recognised a bare delimiter run, but the serializer writes a fence inside a blockquote or a `[!NOTE]` callout with a `>` on every line. Code state was therefore never entered there and the block's interior was cleaned up as prose: `> x = a\_b` round-tripped to `> x = a_b`, losing the author's backslash. Unlike the fence-length cases this one is reachable today — verified against the real serializer before and after. Both fence walks now unquote the line first. INLINE JOINS. Runs concatenated the visible text of every inline tag, so `a<strong>b</strong>c` read as `abc` — a hit that cannot exist in the markdown the indexer scans, where `**` sits between the words. That is worse than a spurious mark: `occurrenceIndex` counts SOURCE occurrences, so a fabricated hit earlier in the document steals the current ordinal and paints the mark on text the search never matched. Only `<br>` continues a run now, because it alone stands for a character the source really has (a `\n`, folded to a space). Everything else stands for syntax the render drops. Nothing real is lost: a match spanning `a**b**c` would have to contain the asterisks to exist at all, and a match wholly inside an element is still found — the element simply starts its own run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(search): match a Note body as it renders, instead of rewriting the file Replaces the serializer change with one that writes nothing. The editor backslash-escapes every markdown-significant character in prose, so a Note the reader sees as `{{TE_SERET}}` is stored as `{{TE\_SERET}}` and search — which matches the stored value — could not find it. The previous approach undid that escape in `postProcessSerializedMarkdown`, which meant re-deriving markdown structure from the serialized string with regexes so it knew what was code. That is a losing game: three review rounds, each finding another construct it did not model (longer fences, then delimiter-prefixed lines, then quoted fences), and each miss REWROTE somebody's code. `markdown-fidelity.ts` is back to staging, byte for byte. The escape is now undone on the matching side only. A field declares `searchTextFormat: 'markdown'` (the Note body is the only one), and the indexer matches it against `projectEscapedMarkdownForSearch(value)` — a total, structure-free function that returns the rendered text plus an index back into the source. Ranges stay in source coordinates, so replace still rewrites the whole `\_` and never strands a backslash. The asymmetry is the whole point: a matcher that de-escapes something a fence would have kept literal changes only which text highlights, and no caller writes it back. A rewriter making the identical mistake corrupts the file. So there is nothing here that needs to know about fences at all. Two consequences worth having: existing notes are searchable immediately rather than after their next edit, and no stored byte changes, so no document the editor has ever written can be affected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a27f376164 |
feat(connectors): add Bitbucket, Databricks, Google Chat, and Workday Help KB connectors (#6895)
* feat(connectors): add Bitbucket, Databricks, Google Chat, and Workday Help KB connectors
Adds four knowledge base connectors, closing the gap where Sim shipped tool
blocks for these services but could not index their content.
- Bitbucket: repository source files and pull request descriptions over the
existing Bitbucket OAuth credential
- Databricks: notebooks (Workspace API) and saved SQL queries, PAT auth
- Google Chat: spaces indexed as message transcripts, new google-chat OAuth
service under the shared Google client
- Workday Help: knowledge article versions via the public helpArticle/v1 API
* fix(connectors): second-pass validation fixes and test coverage
Adversarial re-validation of all four connectors plus a combined-change
regression audit.
- bitbucket: stop declaring incremental sync (deletion reconciliation is
disabled for incremental runs, so deleted files were never removed on the
default code configuration); drop a wasted listing round-trip after the
frontier drains; add 33 tests
- databricks: reject an explicit maxDocuments of 0, which meant unlimited;
add 34 tests
- google-chat: correct the sender displayName documentation (user auth
populates only name and type) and emit second-precision RFC-3339 in the
message filter; 15 -> 24 tests
- workday: fix a crash when maxVersions is persisted as a number; refuse a
configuration whose status filter Workday did not honor; cap the
unresolved-name error; 18 -> 27 tests
- document the Google Chat service-account omission
- docs: list all four connectors and correct the connector count
* fix(connectors): index Google Chat spaces with no messages in the window
Review round 1.
- orderBy takes a full ordering expression, not a bare direction. The reference
documents the default as `createTime ASC`, so send `createTime DESC`; a bare
`DESC` either 400s every hydration or is ignored, which would make the cap keep
the oldest traffic and the later reverse render the transcript backwards.
- getDocument no longer returns null when the message window is empty. A space
with no messages is still a live space, and null is the "document is gone"
signal the engine treats as last-known-good: returning it dropped spaces whose
only prose is their description or guidelines, and left a stale transcript
indexed after a space was cleared or lookbackDays was tightened past every
message.
- The transcript header is omitted when no message contributed text.
* fix(connectors): only flag a Bitbucket listing capped when the cap withheld something
Review round 2.
takeIndexableWithinCap reports capReached as soon as the running total equals
maxItems, which is also true of a listing that ended at exactly that count.
Setting listingCapped there suppressed deletion reconciliation for a complete
listing, so upstream-deleted files and pull requests could stay in the knowledge
base indefinitely. applyMaxItemsCap now takes whether Bitbucket had more content
beyond the page -- a next link, or directories still queued on the frontier --
and flags the listing only when the cap actually withheld something, matching the
Databricks, Google Chat, and Workday connectors.
* fix(connectors): keep the Bitbucket cap flag set when it skips the pull request phase
Review round 3. Fixes a regression from
|
||
|
|
d374ebce6f |
fix(webhooks): accept the methods and expose the request metadata the generic webhook advertises (#6893)
* feat(webhooks): support query parameters and GET deliveries on generic webhooks The generic webhook Setup Instructions promised that query parameters would be available in the workflow and that any HTTP method would be accepted, but neither was true: query parameters were never carried past the route, and every GET that was not a provider challenge got a 405. Carry the request query string into the execution payload and expose it to providers through FormatInputContext. The generic provider merges it into the workflow input under a reserved `query` key, leaving the body's own fields untouched so existing payloads resolve exactly as before. Add an opt-in `acceptsGetDelivery` provider capability and enable it for the generic provider, so a workflow can be triggered by a plain URL fetch such as a link in an email. Providers that have not opted in still answer 405, and unknown paths keep answering 405 on GET so probes cannot distinguish them. Update the Setup Instructions to describe what the endpoint actually accepts. Signed-off-by: mini.jeong <mini.jeong@navercorp.com> * feat(webhooks): expose generic webhook request headers The generic webhook's Setup Instructions promised that request headers would be available in the workflow, but formatInput returned only the body: headers were used solely for the idempotency key and provider signature checks. Expose them under a reserved `headers` key, withholding the ones that carry credentials. Exposing a credential would copy it into execution logs and trace spans, where it outlives the request, so a fixed denylist (authorization, cookie, x-api-key, ...) is combined with the webhook's own configured secretHeaderName. A denylist rather than an allowlist keeps arbitrary custom headers usable, which is the point of the feature. Generalize the query-parameter merge so query and headers share the same key-wise body-precedence rule. Also correct the authentication instruction: only the configured method is accepted, not either one. Refs #6888 Signed-off-by: mini.jeong <mini.jeong@navercorp.com> * feat(webhooks): accept PUT, PATCH and DELETE deliveries and expose the request method The generic webhook's Setup Instructions promised any HTTP method, and the /api CORS policy already advertises PUT, PATCH and DELETE, yet the route answered 405 for everything except POST and GET. Open the remaining methods for providers that opt in, which today is only the generic webhook. Expose the method on the trigger input as well. Without it a workflow behind one URL cannot tell a create from a delete, which makes multi-method delivery half a feature. The payload field is optional so jobs already queued at deploy time keep executing. Turn the GET-only opt-in into a per-provider method set, and let the request metadata merge carry scalar values so `method` follows the same key-wise body-precedence rule as query and headers. Refs #6888 Signed-off-by: mini.jeong <mini.jeong@navercorp.com> * feat(webhooks): declare the generic webhook trigger outputs The trigger declared no outputs, so the reference dropdown in the editor offered no completions for it and users had to type paths like `query.id` by hand after reading the setup instructions. Declare the request metadata that is known ahead of time. Body fields stay undeclared because a generic webhook receives whatever JSON the caller sends. Refs #6888 Signed-off-by: mini.jeong <mini.jeong@navercorp.com> * fix(webhooks): stop provider challenges from intercepting other providers' deliveries The challenge handlers run before webhook lookup and are provider-blind, so two query parameter names are effectively reserved across every path. Now that a generic webhook can be triggered by a URL fetch, a link carrying either name answers the challenge instead of running the workflow: - `?validationToken=x` is echoed back as a Microsoft Graph subscription validation. Graph sends that validation as a POST, so ignore the parameter on every other method. - `hub.mode`, `hub.verify_token` and `hub.challenge` answer 403 when no WhatsApp webhook on the path expects a token. A path with no such webhook is not a failed verification - the parameters belong to whoever owns that path - so fall through and let the delivery route normally. A token mismatch against a WhatsApp webhook still fails with 403. Refs #6888 Signed-off-by: mini.jeong <mini.jeong@navercorp.com> * fix(webhooks): make the request metadata opt-in per webhook The four commits below make the generic webhook do what its Setup Instructions promise. They do it through a provider-level capability, which applies to every generic webhook row the moment it deploys: each one begins accepting GET, PUT, PATCH and DELETE, and each one's workflow input gains `method` and `headers`, on POST deliveries too. No webhook owner chose either. Gate both behind `providerConfig` flags written by two switches, off by default. A webhook deployed before these existed has neither flag, so it answers POST only and its input is exactly the body, as before. `query` stays ungated: it is dropped today, only appears when the caller's own URL carries it, and yields to a body field of the same name. Generalize the Microsoft Teams challenge fix. Every challenge handler runs before the webhook lookup and matches on payload shape alone, so any of them will answer a delivery addressed to another provider on the same path. Gate them centrally to POST via `challengeMethods`, which WhatsApp widens to GET for Meta's handshake, rather than guarding one handler inline. Also: - Widen the credential header denylist to 24 names and withhold the webhook's own token by value as well as by name, since a denylist is leaky by construction. - Condition the `method` and `headers` trigger outputs on their switches, so the reference dropdown cannot offer a field the webhook will not send. - Give PUT, PATCH and DELETE their own contracts instead of reusing the POST one, whose `method: 'POST'` had become untrue. - Parse, challenge and generate a request ID once per delivery rather than twice on GET, which was logging one request under two IDs. - Offer the challenge handlers the request before admission, so Meta's GET handshake cannot be answered with a 429 by a busy instance. - Answer every non-POST rejection with the same 405 plus `Allow`, whether the path is unknown, holds only non-path triggers, or holds a trigger that has not opted in. - Read flags through a helper treating only `true`/`'true'` as on: the editor writes booleans, but a YAML- or Copilot-authored workflow can write the string `'false'`, which is truthy. - Name the methods switch "Accept Other HTTP Methods": HEAD and OPTIONS still answer 405, so claiming "all" would reintroduce the overstatement this whole change set exists to remove. - Drop the per-delivery metadata warn logs to debug. --------- Signed-off-by: mini.jeong <mini.jeong@navercorp.com> Co-authored-by: mini.jeong <mini.jeong@navercorp.com> |
||
|
|
d9cfd7c68e |
improvement(mothership): v0.9 (#6815)
* checkpoint
* Checkpoint
* dot fixes
* Make async tool resume delivery recoverable
* Support split table tools and option recovery
* Harden VFS mutation handling
* feat(platform): platform subagent support — docs corpus VFS, search_docs, account context
Squash of the feat/platform-agent branch (sim side): mounts the Sim docs
corpus in the copilot VFS, wires search_docs and retires the legacy docs
search tools, and syncs the generated tool catalog and trace contracts for
the platform subagent.
* Align Copilot tools and resource handling
* Expand workflow log query support
* checkpoint
* Port desktop-improvements-0 desktop and browser-agent work
* fix(desktop): keep browser-agent input alive through live-SPA re-renders
* Harden workflow sanitization and Slack setup
* feat(desktop): coordinate clicks, caret insertion, and drag for the browser agent
* Revert subagent group eager auto-collapse
* fix(chat): keep sends FIFO across the streaming-to-idle drain gap
* Add the steering backend surface for mid-turn sends
* Sync generated contracts for async subagent orchestration
Pulls the mothership tool catalog (wait_agents / tail_agent / steer_agent /
interrupt_agent), trace spans (chat.async_subagent.*, chat.orchestrate.*), and
trace attributes (copilot.async_subagent.*) into the generated TS contracts.
* Add display titles for the async subagent orchestration tools
wait_agents / tail_agent / steer_agent / interrupt_agent get natural-language
running titles (naming the agent id being waited on, tailed, steered, or
stopped) and a Steering→Steered completed-verb rewrite.
* Show orchestrator-chosen subagent names on agent groups
A subagent_start whose payload data carries a name (the orchestrator's new
name trigger parameter) now labels the agent group with that mission name —
the agent-type icon stays. The name flows through the live stream path, the
turn model (AgentNode.displayName) and its serialize/rebuild round-trip, and
persisted transcripts (PersistedContentBlock.name), so reloads keep the label.
* Improve Copilot error handling and logging
* Backfill the subagent display name from the second start event
The dispatch-time subagent_start fires before the trigger args (and therefore
the name parameter) have streamed; the phase-3 start re-announces the lane with
the name. The block builder was dropping that duplicate wholesale, losing the
name on streaming providers — now it backfills subagentName onto the existing
block instead. (The home turn-model path already reconciled this case.)
* Support Slack bot connection flow
* Harden Copilot error and VFS handling
* Harden VFS resource operations
* Show 'Waiting for the first of N agents' for mode-any waits
The wait_agents title ignored the mode argument, so an any-mode wait over
three agents read 'Waiting for 3 agents' while the model narrated waiting for
the first — contradicting the transcript.
* Collapsed-by-default agent cards with live intent status lines
Subagents now narrate their work through <intent>3-5 words</intent> tags (a
fleet-wide prompt protocol on the mothership side). The turn model streams
each subagent's text through a split-safe tag parser: complete tags update the
agent's currentIntent and disappear from the prose, tags split across deltas
are carried until their close arrives, and a tag that never closes flushes
back as plain text.
The agent card renders as one line — display name (or agent label) plus the
latest intent, replaced inline as the agent shifts gears — and never
auto-expands; expanding to the full tool log is a deliberate click. Only an
outstanding permission prompt or a browser hand-back forces a group open.
Intents persist on the subagent block (and through the legacy persisted-
message paths) so reloads keep the last status, and a renamed reinvocation
now takes the latest name instead of pinning the first.
* Add the internal in-band tool execution route for live mothership turns
POST /api/copilot/tools/execute (INTERNAL_API_SECRET, Go→Sim) runs one
sim-server tool through the same server tool router the resume driver uses
and returns the result synchronously — no checkpoint. This is what lets
background (async) subagents write files/tables/knowledge, and lets the main
lane keep streaming (instead of checkpoint-pausing and killing every
background run) while async agents are live.
* Persist resource side effects for in-band tool execution
Files/tables created through the internal execute route now register on the
chat's resources exactly like the resume driver's executions — the route runs
the same handleResourceSideEffects pass (persistence only; an out-of-band
route has no live event sink, so mid-turn chip pushes are a follow-up).
* Extract intents from group text on every path, sync and async
The turn-model intent filter only fires for span-scoped subagent lanes, but
this surface also delivers subagent text through the legacy block path — so
<intent> tags flowed through unparsed and rendered as prose rows. Groups now
extract intents from their accumulated text at append time: the last complete
tag becomes the card's status line and every complete tag is stripped from
the rendered prose. Covers span-scoped, legacy, and persisted-reload paths
for both synchronous and background delegations.
* Fall back to the live tool title for the agent card status line
Persisted data proved tool-first subagents (grok search agents) emit zero
prose, so intent tags never stream no matter what the prompt says. The
collapsed card now always narrates: the agent's own <intent> tag when present,
else the latest tool's display title while the lane is live.
* Catch subagent <intent> tags in the server relay
The relay's subagent text handler now runs the split-safe intent extraction
as chunks stream: the latest complete tag is stamped onto the lane's persisted
subagent block (subagentIntent) and stripped from the stored prose, so live,
persisted, and replayed views all agree. Per-lane carry handles tags split
across chunks; a never-closing tag flushes back as plain text.
* Drop the tool-title fallback: the status line is the agent's intent
With the intent protocol now injected into every spawn's task message, agents
open with an <intent> tag; the card shows that narration or nothing.
* Replace intents with live tool-title status lines on agent cards
Intent parsing is fully removed (turn model, relay handler, persistence
fields, group extraction). The collapsed card's status is the latest tool
call in its RUNNING phrasing — never the completed rewrite, which stays in
the expanded log. Parallel tools show the most recently started still-running
title with a +N for concurrent siblings; between rounds the last title stays
frozen; a closed lane shows the bare name. Nested agent cards compute their
own status recursively from their own items.
* Keep the main Sim lane live-expanded; collapse only real subagent cards
The mothership group is the turn's own narration, not a delegation card —
collapsing it hid main-lane text and tools until manual expand, which read as
mis-ordered streaming while async subagents interleaved. It keeps the
original live-expand behavior and no status suffix.
* Persist subagent lane lifecycle blocks from the span handler
Lane-scoped span events route to the span handler, which only recorded trace
side effects — no subagent start block was ever persisted (verified: a
seven-agent run stored 104 blocks with zero starts). Grouping then fell back
to keying lane content by agent NAME, so a respawned agent of the same type
merged invisibly into the first one's card until it resolved. The handler now
persists the start block (spanId-keyed and deduped, carrying the display
name) and stamps endedAt on close, giving every invocation its own card.
* Name agents in orchestration titles; '+ n more' overflow format
wait/tail/steer/interrupt titles humanize the slugified agent ids back to
their display names ('Waiting for the first of Digest Workflow Build + 4
more'), and the agent card's parallel-tool suffix uses the same '+ n more'
format.
* Harden in-band tool execution and resources
* Route in-band execution through the comprehensive tool dispatcher
The internal execute route used the bare server-tool router, which rejects
VFS tools with 'Unknown server tool: read/glob/grep' — so nearly every
background agent's first discovery call failed (102 in-band calls in one run,
dozens rejected). It now uses the relay's executeTool dispatcher: registered
handlers (VFS, function execute) with permission checks and param
normalization, falling back to the app tool router — the same surface
foreground execution gets.
* Harden chat stream transition handling
* Harden VFS provenance and resource writes
* Standardize tool environment references
* Harden browser panel and chat cleanup
* Descriptive, user-language tool titles across the board
House rules applied everywhere: use every argument the call carries, never
name internal machinery, and never lead with Getting (the Got rewrite is
deleted so it cannot return).
- Deployments name the workflow: Deploying {workflow} as API/chat app/MCP tool
- Workflow reads name the part: Reading {workflow} meta/state/deployment/notes;
generic reads always name the file (Reading {leaf}), never bare Reading file
- Block runs name block and workflow: Running {block} in {workflow}, Running
from {block} in {workflow}, Running {workflow} until {block}, and
Enabling/Disabling {block} in {workflow}
- The six split-table tools get per-operation verbs (Adding column {name},
Updating rows, Wiring automation, Creating view {name}) instead of a wall
of Querying table
- The manage quartet drops X-action system-speak for gerunds
- get_* internal names become user language (Checking run settings, Tracing
block inputs, Reading the deployed version); web_fetch says Fetching
- Scheduled-task titles removed entirely (feature deleted from the Go catalog)
- New verb rewrites: Fetched, Traced, Wired, Configured, Looked, Rotated
* Deploying {workflow} as chat, not as chat app
* Loader gerunds; mv names both ends; mkdir names the folder
search_integration_tools -> Finding the right integration;
load_integration_tool -> Loading {integration} tools; load_skill ->
Loading skill {name}; run_enrichment -> Looking up {subject}. mv prefers the
model's phrasing, else reads 'Moving {files} to {destination}'; mkdir reads
'Creating folder {name}' from the path.
* Overflow counts read '+ n', dropping 'more'
* Unify workspace find and search
* Scale desktop title bar with page zoom
* Serialize account and organization truth into the copilot VFS
Workspace standing, membership, billing, org role, access-control
restrictions, published-block provenance, and fork topology were reachable
only through three parameterless tools (or not at all). They are ambient
read-only facts, so they belong in the VFS where they are greppable, cost no
tool round-trip, and every agent that can read gets them — the same move that
retired get_blocks_and_tools and list_user_workflows.
Adds account/{workspace,workspaces,members,billing}.json (always mounted) and
organization/{organization,access-control,custom-blocks,forks}.json (only when
the workspace is org-hosted). Every file projects an existing use case or util
after getOrMaterializeVFS's access assert — no new queries, no new
authorization. One relation per file, cross-referenced by id-and-name stub, so
overlapping facts cannot disagree. Volatile content (billing, access control,
forks) is lazy, so numbers are read-time fresh and unasked-for reads cost
nothing.
Projection follows the viewer: member emails are admin-only, fork detail
requires workspace admin on a forking-enabled org, and the whole organization/
namespace is absent for a personal workspace — which is itself the answer.
Retires get_account_billing, get_enterprise_context, and list_user_workspaces
along with their handlers; display titles stay for transcript replay.
* Fix insert_text refusing an editable field focused inside a frame
describeFocusedEditable descended shadow roots but not frames, while
activeElementReadback descends both. Focus inside a same-origin frame therefore
surfaced to the first as the FRAME element — not an input, not contentEditable,
not a canvas, no textbox role — so it fell through to 'not-editable' and
insert_text refused a field that press_key had just typed a character into.
Two functions answering 'what is focused' with different answers is the bug;
the descent loops now match exactly.
The refusal also names what actually held focus (tag, role, contenteditable).
A bare 'not-editable' gave the agent nothing to act on, so it guessed at the
cause — a real run spent twenty rounds on the wrong theory and had to be
stopped by the user.
* Keep retired browser takeover renderable in history
The tool is gone from the catalog, so its generated constant went with it and
every path that referenced it stopped compiling. Deleting those paths instead
would have silently downgraded every past transcript containing a takeover card
to a generic tool row, and dropped the no-timeout budget that an in-flight
takeover still needs while a rolling deploy finishes.
retired-tools.ts gives the literal a documented home that says what it is and
why it survives its tool.
* Follow the agent into a tab it opened to work in
browser_open_tab created the page with activate: false, so the agent worked in
a tab the user could not see while the panel sat on a page where nothing was
happening. The panel now follows a tab the agent deliberately opened.
Scoped to that tool only. A page spawning its own tab (popup, target=_blank) is
the site grabbing the view rather than the agent choosing a workspace, and
stays in the background as before — two existing tests pin that and caught the
first version of this change, which moved both.
A tab the user claimed still wins over both: the work starts in the background
instead of pulling the page out from under them mid-read.
* Make the browser tools agree with each other
An audit of the module found the frame-descent bug was one instance of a
pattern: six independent definitions of 'is this editable' and seven of 'what
is focused', disagreeing with each other. A tool refusing what its sibling
accepts on identical page state is invisible at runtime — the agent follows a
snapshot that says one thing into a tool that says another.
- browser_type now accepts role="textbox" like browser_insert_text does. The
snapshot advertises those elements as [textbox] with a ref, so refusing them
meant rejecting exactly what the outline told the model to type into. Both
the native and synthetic paths, and their descendant scans.
- pressKeyOnPage descends shadow roots and frames like every other focus
reader. It was dispatching synthetic keys at the shadow host or <iframe>
element, where they bubble but never reach the editor, while reporting
success — and contradicting the activeElement reported beside it.
- not-editable and ambiguous-editable name what was found: the element's tag
and role, and the candidate fields. Both had the data and discarded it, which
is what turns one blocked step into twenty rounds of guessing.
- obstructedAfterNavigation requires a dialog that ARRIVED with the
navigation. It compared against nothing, so every SPA route change under a
persistent role=dialog reported a successful click as obstructed. The test
that covered this asserted the false positive; it now pins both directions.
- browser_insert_text observes the top document when typing inside a frame,
like every other input tool. A submit that navigates the top page was
invisible to its frame-scoped observation.
* Let hover actually see what it mounted
Four independent defects made browser_hover blind to the most common thing a
hover produces — a row's action bar — so it reported no effect on a hover that
worked, and the agent fell back to clicking pixels off screenshots.
- The popup scan matched only role=tooltip/menu/listbox. Slack's message
shortcuts bar is a labelled toolbar/group, so it registered as nothing at
all. Added toolbar, menubar, labelled group, and [popover].
- The baseline was captured BEFORE prepareElementSurface scrolled the target
into view, so scrollChanged was always set by the tool's own probe. That
pinned every unproductive hover to 'background DOM churn' instead of the
honest 'nothing happened', and hid scrolling the hover really caused.
Re-baselined once the scroll settles and before the pointer moves.
- The MutationObserver attached only on the first observation, while the roots
list is rebuilt every call and grows as shadow roots mount. Components that
appeared later were never observed, so their DOM changes raised no revision.
Roots are now observed as they show up.
- observationTruncated was computed and never read, so a scan capped at 12k
nodes reported 'nothing appeared' with the same confidence as a complete
one — and portalled overlays live at the end of <body>, exactly what the cap
drops. Hover now says the page was too large to scan and to confirm visually.
* Stop the browser agent acting on the wrong element, and say why it refused
Four findings from the module audit, the first of which could silently do the
wrong thing rather than merely fail.
- A ref whose node is gone is re-adopted by structural resemblance, matching on
ORIGIN only so a pushState between snapshot and act does not kill every ref.
That leniency also let a ref to a row control in one view rebind to the
identical control in a view the app had since navigated to — acting on the
wrong message, signalled by nothing louder than recovered: true. Adoption now
requires the same path; a view swap reports the ref stale, and the caller
re-snapshots. Revalidating a still-connected node stays lenient, because that
is literally the node the model chose.
- A hit INSIDE the requested element is its own nested control, not an overlay.
Both produced 'covered by X — close or move the overlay', advice that cannot
be followed because there is nothing to close. Nested hits now say so and
point at retargeting.
- browser_click_at, browser_insert_text, and browser_drag listed targetChanged
in their effect formulas, but none passes an elementId, so no targetState is
ever captured and the term was always false — coverage that read as real.
Removed, with a test pinning the dependency.
- The seven effect formulas are deliberately NOT collapsed into one predicate:
drag must trust domChanged where others must not, hover must ignore field and
focus changes, click counts focus only for editables. Forcing one would make
each tool wrong differently. The differences are now documented in one place
next to the shared computation, so divergence is a declared policy rather
than an accident.
* Let edit_workflow configure block retries
* Updates
* Always focus the resource the agent is working on, and its browser tab
The resource panel had a carve-out: an already-open browser session declined
to replace another selection and only got an attention marker, so agent
browser work happened off-screen. The panel now follows the agent to whatever
it touches — browser included — and an event can still opt out explicitly.
The browser panel also follows the agent BETWEEN tabs: the store already
tracked automationTabId (and the strip marked it), but the visible tab never
changed. It now switches when the agent's target tab changes, so watching the
agent never means hunting for the tab it moved to. Keyed on the target
changing rather than on it being set, so a user who browses elsewhere
mid-run is only pulled along when the agent itself moves.
* Never paint a browser snapshot at stale geometry (the modal-open flash)
Opening a modal locks scroll, which removes the window scrollbar and reflows
the panel — so a capture taken before the lock describes a rect the panel no
longer occupies. The handshake painted that frame anyway and only then
retried, so the replacement landed visibly offset from the page it stands in
for: the flash. A capture is now checked against the host's live rect before
it is painted; a mismatched frame is skipped and re-captured at the settled
layout instead (modal retries go 2 -> 3 to absorb the extra settle).
* Name the workflow in deployment and workflow-scoped tool titles
'Checked deployment status' never said which workflow — nor did the deployed-
state read, run settings, block outputs/inputs, redeploy, promote, or the
global-variable write. These tools carry a workflowId (often defaulting to the
current workflow), so only the client can resolve a name: the enrichment layer
now resolves it for the whole workflow-scoped family and passes it as
workflowName, which every workflow title already reads.
Titles: Checking {workflow} deployment status, Reading deployed {workflow},
Checking {workflow} run settings, Reading {workflow} block outputs, Tracing
{workflow} block inputs, Redeploying {workflow}, Promoting {workflow} version
{n} to live, and 'Adding workflow variable {name} in {workflow}' — each
falling back to its unnamed form when no workflow resolves.
* Name the block that ran; never fall back to a raw block id
run_block and set_block_enabled carry only a blockId, so their titles would
have printed an opaque UUID ('Running 7f3a2b91-… in Invoice Sync'). The
enrichment layer now resolves blockId against the workflow store the same way
it already did for run_from_block's startBlockId, and the base titles no
longer accept an id as a name — an unresolved block reads 'Running block'
rather than a UUID.
* Add the missing Removing -> Removed rewrite
The table work introduced 'Removing automation'/'Removing enrichment' with no
past form, so those rows kept their present tense after completing.
* Name the target resource in the remaining tool titles
Table tools keep their operands nested under args and identify the table by
id, so their rows said 'Adding rows' with no hint where: enrichment now lifts
the nested args and resolves tableId against the cached workspace table list,
giving 'Adding rows to Runtimes', 'Adding column status in Runtimes',
'Reading views of Runtimes'.
Also: a block-schema read names the block instead of the file ('Loading
Slack', 'Loading Google Sheets tips'); browser type/insert show the text they
send, middle-ellipsized; downloads name the file; library-docs searches name
the library and query; knowledge-base searches include the query; generated
media names its output file; and diff_workflows, list_deployment_versions,
and publish_custom_block joined the workflow-name enrichment set.
* Bubble nested agents' tool calls into the parent's status line
The collapsed status only scanned a group's OWN tool items and skipped nested
agent groups, so a parent that had delegated froze on its last own tool while
its child did the actual work — the line described nothing that was running.
Status now walks the whole subtree: any tool at any depth counts, the most
recently started running one is shown, and the rest become the same '+ n'
overflow. With nothing running it falls back to the last tool at any depth,
so an idle parent still reflects where its subtree got to.
* Align nested tool call status rows
* Revert branch-local KB connector error-message edits
Restores apps/sim/connectors/ to staging state. Two copilot-focused commits
on this branch (
|
||
|
|
6f34dd6248 | fix(credentials): hide gated service accounts from connected list (#6898) | ||
|
|
43850e3567 | improvement(tables): disable the default view's delete action with a tooltip (#6897) | ||
|
|
97c1688c49 |
feat(modal): add Modal Labs integration (#6896)
* feat(modal): add Modal Labs integration Modal has no public REST control plane — the Python/JS/Go SDKs all speak gRPC — so this covers the two surfaces that are reachable over HTTP: deployed Web Functions/Servers, and the OpenAI-compatible Endpoints API. Three operations: call a deployed function with proxy-token auth, generate a chat completion on an Endpoint, and list the models a token can reach. Auth sends the token pair as Modal-Key/Modal-Secret rather than the combined bearer form, so a Web Function that validates its own bearer token keeps the Authorization header free. Both URL fields require https since Modal terminates TLS everywhere, and a cleartext URL would leak the token. Chat completion declares request.modelInput so the system prompt and user message project to canonical placeholders before egress. Call Function deliberately does not — a Web Function runs arbitrary user code, and nothing proves its body reaches a model. /v1/models fields beyond `id` are inferred from OpenAI compatibility rather than printed in Modal's docs, so they are marked optional. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(modal): type the wire payloads and default chat to the shared endpoint Chat Completion required an endpoint URL and passed a blank one straight into modalOpenAiUrl, which throws — while List Models already fell back to the shared inference host and the generate-on-modal-endpoint skill tells agents to leave the field empty for Shared Endpoints. Skill-driven chat calls against the shared host failed instead of using that default. Chat now falls back the same way and the block field is no longer required. Replaces every `any` in the Modal tools with declared wire types for the OpenAI-compatible /v1 payloads. Fields stay optional because the shape comes from whichever inference engine backs the endpoint, so the readers keep their defensive `??` guards — the types exist so a future change to that mapping fails the compiler instead of shipping. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3b4d9e9149 |
fix(schedule): reconcile interrupted schedule executions (#6780)
* fix schedule execution recovery * simplify schedule recovery provider handling * fix(schedules): stop reconciliation rewriting healthy carriers and index its scan Follow-up hardening on the schedule recovery reconciliation. Reconciliation settled every carrier it examined, including ones that had already reached a terminal status and recorded their own outcome. Because the normal completion path never stamps the reconciled marker, each successful run's carrier was picked up on the next tick and rewritten: `completedAt` bumped, `error` nulled and `output` replaced with a recovery stub, all of which `GET /api/jobs/[jobId]` surfaces. A completed carrier whose execution log had aged out was additionally flipped to failed. Settle only carriers still in flight; a terminal one is owed schedule accounting and the marker, nothing more. The recovery scan matched no index. There was none on `async_jobs.updated_at`, and the unreconciled-terminal branch tested a jsonb extraction, so the whole OR fell back to a sequential scan and sort of `async_jobs` on every tick. Add the partial index, and spell the branch's status list and metadata key as SQL literals: Postgres cannot prove a parameterised predicate implies a literal index predicate, so a bound key would have left the new index unused. Irrecoverable carrier tombstones were exempted from retention with no secondary expiry, so the one class of row that can never reconcile grew without bound. Give them a longer bounded window instead. `WORKFLOW_EXECUTION_MAX_ROWS_PER_RUN` had become a per-status budget when the stale-execution sweep gained its `redacting` pass, silently doubling the per-run cap. Share the budget across both passes, and add the matching partial indexes so the second pass keeps an index rather than seq-scanning. Also consolidates the carrier metadata keys, their predicates and the jsonb merge into one module -- the two routes were the writer and reader of the same keys with no shared symbol, so a rename type-checked clean while silently breaking retention. * fix(schedules): rotate deferred carriers and stop the redacting sweep failing live runs Addresses the review findings on the reconciliation hardening. A carrier whose accounting was deferred got no write at all, so it kept its `updatedAt` and stayed at the head of the `updatedAt`-ordered recovery batch on every tick, starving every other claimed carrier and never becoming eligible for retention. This was a regression from the previous commit: settling used to bump the timestamp for every examined row, and skipping the settle for already-terminal carriers removed the bump with it. A payload with no `scheduledFor` can never reconcile, so such a row pinned a batch slot permanently. Bump `updatedAt` unconditionally and keep only the reconciled marker conditional. The stale-execution sweep terminalized `redacting` logs on the execution deadline. That deadline bounds execution, while `redacting` covers payload masking after the run already finished -- so a run that used most of its budget entered redaction with the deadline due, and the sweep failed it five minutes later while the worker was still masking. Schedule recovery then read the log as a failed occurrence and counted a failure that never happened, even though the worker's terminal write later restored `completed`. Sweep `redacting` on the generic stale window only. `getScheduleNextRunAt` falls back to a daily cadence when a schedule has no cron expression. Deployment cannot persist such a schedule, so the branch is unreachable, but this change widened its use from failure recovery to every outcome -- log a warning when it fires rather than silently guessing a cadence. `executionDeadlineAt` was missing from the shared `workflowExecutionLogs` schema mock, so it read as `undefined` and assertions comparing against that column were trivially true. Add it. * refactor(schedules): drop vestigial recovery code and close a builder gap Follow-ups from a full re-read of the change. No behavior change except the removed dead code paths. The metadata merge stripped a `scheduleRecoveryBlocked` key on every write. That key has never been written by any shipped code -- it appears nowhere in staging and nowhere in history outside this branch -- so the strip guarded against a state that cannot exist, at the cost of an extra jsonb operation and a bind parameter on every reconciliation write. `processScheduleItem` set `carrierObservedOrLookupUncertain` immediately before returning on an ambiguous enqueue. The flag is only read from the surrounding catch block, which a normal return skips, so the assignment was dead and read as though it were load-bearing. Replaced with a comment stating why the occurrence is preserved. The stale-execution sweep carried two near-identical `jsonb_set` templates that differed only in their error expression, kept flat because the test mock renders nested SQL fragments as placeholders. The suite now has a recursive renderer, so the error expression is a named per-status value and there is one `jsonb_set`. Success was the only schedule outcome without a named update builder, which left `executeScheduleJob` using two idioms for the same guarded write and left the update shape untested. Add `buildScheduleSuccessUpdate` beside its cancellation and failure siblings, use it from both call sites, and cover it the way `buildScheduleCancellationUpdate` is covered -- a mutation of its `failedCount` reset previously passed every suite. --------- Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local> Co-authored-by: Waleed Latif <walif6@gmail.com> |
||
|
|
85902eb7ff |
feat(tables): improve view and filter controls (#6725)
* feat(tables): improve view and filter controls * fix(tables): keep menu actions open * fix(tables): guard view autosave against echo remounts and stale responses Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tables): keep and/or filter toggles, autosave only real edits Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tables): compute view-row action spacer and cover the default pin Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(tables): apply filter text on enter or blur instead of a debounce Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tables): apply filter edits from user events * fix(tables): reject stale default promotions * fix(tables): preserve filters during rule transitions * fix(tables): isolate flagged view interactions * fix(tables): preserve OR boundaries when rules drop * fix(tables): retain deferred filter conditions * fix(tables): keep hidden view actions click-through * fix(tables): keep disabled view pins from selecting the row A disabled Button is pointer-events-none, so clicks on the default or read-only pin fell through the overlay to the row and selected the view. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
38630ff46d |
feat(tables): autosave persisted default views (#6724)
* feat(tables): autosave persisted default views * fix(tables): close persisted view lifecycle gaps * fix(tables): preserve edits through view hydration * fix(tables): preserve the persisted default owner * fix(tables): preserve legacy layout through view adoption * fix(tables): close final view autosave races * fix(tables): preserve valid saved sort on stale links * fix(tables): make the first saved view the default Creating the first view left isDefault false, so the legacy All fallback stayed in the menu instead of handing off to the new view. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tables): refuse to delete a table's last saved view The sibling check and the delete share the views advisory lock, so racing deletes cannot drop a live table to zero views and regress it to the legacy "All"-only state. Views of a hard-deleted table are removed by the FK cascade, which this guard never sees. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
785c619b1c |
improvement(provenance): name the block behind an unprojected input root (#6890)
* fix(provenance): name the block behind an unprojected input root
structural-input-root-unprojected fires when a block's config.params
throws on the projected inputs — the copy where a secret has been
replaced by its placeholder — and no structured projection recovers it.
It was reported with the reason and nothing else, so a line told you
this had happened somewhere without naming the block, and the caught
error was discarded by a bare catch. markIncomplete now takes a
structural detail, and this guard passes the block type, tool, input
path, and failure class.
Names and types only. A coercion that rejects a value tends to quote it,
and an input reaching this guard may still hold a resolved secret.
Which is also why the json-parse warning a few lines above no longer
logs the thrown message: V8 quotes the text it rejected back into it —
Unexpected token 's', "sk-live-EX"... is not valid JSON — and that
prefix is enough to leak. The field name and its declared type are
already in the message, and SyntaxError is the only class JSON.parse
throws.
* fix(provenance): keep a detail from displacing the reason it explains
The detail merged into the incompleteness payload could shadow `reason`.
Spreading it first at the call site protected only the fields added
there; `reason` is added a level up in reportIncompleteness, which
built `{ reason, ...details }`, so a detail carrying that key replaced
the guard literal on the line while the level was still selected from
the real one. `origin` was reachable the same way whenever no importer
origin was set.
Write `reason` last, which protects every caller of that reporter
rather than the one that prompted this, and close the detail to named
fields so neither key is expressible without a cast.
|
||
|
|
f5728fa887 | fix(icons): restore the Crunchbase mark's counter and framing (#6887) | ||
|
|
4214a891f4 |
fix(setup): publish unscoped setup package (#6886)
* fix(setup): publish unscoped setup package * fix(setup): strip renamed status command |
||
|
|
f6a9f0dd87 |
fix(integrations): white CB Insights tile and a borderless Crunchbase mark (#6884)
CB Insights moves from a dark navy tile to white, matching Jira, Confluence, and Bitbucket. Its icon carries its own fills, so it stays legible on the lighter tile. The Crunchbase icon drops the white rounded-square plate and its border, leaving just the `cb` mark on `currentColor` so the block's bgColor supplies the tile. The viewBox is retargeted to the glyph's true curve extrema with padding that keeps it at the same optical weight as the surrounding brand marks. |
||
|
|
4fce5190e6 |
fix(bitbucket): bind cursors and task locations case-insensitively (#6883)
* fix(bitbucket): bind cursors and task locations case-insensitively Bitbucket resolves workspace and repository slugs case-insensitively but echoes the canonical lowercase form in `next` links, diff/diffstat redirect targets, and async merge task Locations. Binding those back with exact string equality meant a mixed-case slug succeeded on the first request and then failed on every follow-up — worst on a 202 merge, where the merge has already started when polling breaks. Repository file paths keep verbatim comparison; git treats those as case-sensitive. * fix(bitbucket): fold only the slug segments Bitbucket canonicalizes Segment-wise comparison replaces whole-path case folding, so a cursor that recases a fixed endpoint literal (repositories, commits, pullrequests) fails locally again instead of being deferred to Bitbucket. Only the workspace and repository segments of a /2.0/repositories path fold; file paths and literals stay verbatim. |