v0.8.48: search improvements, file versions - #8008
waleedlatif1 wants to merge 34 commits into
Conversation
* docs(blog): update enterprise * Pi Babysit: address PR #8002 feedback * Change author in enterprise blog index Updated author name from 'andrew' to 'vik'. --------- Co-authored-by: Sim Pi Agent <pi@sim.ai> Co-authored-by: Waleed <walif6@gmail.com>
… from task failures (#8004) * fix(file-search): skip base64-dominated files and redact query errors from task failures * fix(file-search): count wrapped base64 blocks and verify trigram estimate against pg_trgm
… MCP (#7997) * feat(files): workspace file version history over the v2 API, CLI, and MCP * fix(files): address review feedback on file version history * chore(db): regenerate file version migration after staging's 0364 * improvement(files): scope version rows to the write's workspace and narrow stored provenance status * fix(files): release purged file history atomically with restore * fix(files): release file history through the outbox after the current object is gone * fix(files): release purged history inside the file-row purge transaction * fix(files): resolve metadata version from the record's own storage key * fix(files): never pair a stale file record with a newer version number * improvement(files): simplify file version history internals - read metadata and its current version in one statement; drop the retry and 409 - move provenance policy branching into the provenance module - project stored provenance out of list, head, and get reads; revert reads it on demand - chunk storage-cleanup enqueues to the outbox bulk limit in one place - drop the write-only content_updated_at version column (unreleased 0365) - reuse findCause, the shared cleanup batch constants, and the version-number primitives - share the v2 text presenter between the file and version routes
…8006) * improvement(knowledge): remember a caller's saturated search reach A caller whose tokens reach more documents than the permitted-set limit paid the reach count on every search only to learn again that the set is unbounded. That answer is now remembered per bases and token set for five minutes. The probe now reports saturation apart from a timeout, and only saturation is remembered; an unbounded set only means the legs apply the full access predicate per candidate, so a stale answer costs speed, never access. * improvement(knowledge): keep the permitted-set resolver's TSDoc on its function
) * fix(knowledge): keep Slack searchable while its member crawl runs Members-mode search shows a document only while its member observation is younger than a day, but a full Slack listing re-fetched every thread and so took far longer than a day on a large workspace, leaving most of Slack invisible. Access is now renewed per channel the member can still read, and listings re-read only threads whose root changed, are active, or are due in a rolling 28-day refresh. * fix(knowledge): keep scope renewal due when a channel listing is cut off A Slack conversation listing that stops at its page cap now reports itself incomplete, and the member's renewal watermark only advances once every reachable channel has been listed and renewed. * fix(knowledge): restart listings for Full resync and reset renewal on identity change A Full resync now starts a new full-sync listing even when the connector does not rehydrate, so Slack rereads every thread instead of resuming an ordinary cursor. A member whose identity changes or whose token is rejected also loses its scope-renewal watermark, so renewal runs for the new identity right away. * fix(knowledge): resume member scope renewal across runs Accessible scopes are now listed page by page, and a renewal that does not finish within its budget stores its channel-listing cursor and start time, so the next run continues from there instead of re-reading the first pages and never reaching channels past them. The watermark records when the whole pass began, and an expired cursor restarts the pass once. * fix(knowledge): batch scope renewal and keep refreshed metadata Scope renewal now gathers container pages into batches before scanning the member's stale observations, so the scan runs once per batch instead of once per source page, and an unfinished batch resumes from where it was read. Content that hydrates unchanged under a new hash also refreshes its source URL, modified time and tags, and the member sync log records how many observations renewal kept fresh.
) A member who reaches more of an organization search index than an exact ranking can afford searched keywords through the GIN projection, which scores every chunk matching the term before access is checked, so a common word cost seconds. Where the database provides the tin extension, a BM25 projection of search-index chunks is kept by embedding and knowledge-base triggers, and the keyword leg ranks with Tin first and checks access only on the top of that ranking, widening the window while too few ranked chunks are readable and leaving a page to GIN if the widest window cannot fill it. The query is analyzed by the same websearch_to_tsquery as the GIN path and translated to TINQL; shapes TINQL cannot express keep GIN. The path is gated by the knowledge-tin-keyword flag, a valid Tin index (built only after the backfill completes), and every base being a search index. Script migration 0019 installs the projection only where tin is available and creatable, so self-hosted databases keep an empty table and the GIN path.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
| const reread = | ||
| syncContext?.fullSync === true || | ||
| now / 1000 - lastRootActivity(message) < ACTIVE_THREAD_REFRESH_SECONDS || | ||
| dueForRollingRefresh(externalId, now) |
There was a problem hiding this comment.
Slack replies remain searchable
For quiet threads, this code skips hydration whenever the root-derived version is unchanged. That version includes only the root edit timestamp, reply count, and latest-reply timestamp, so editing or deleting an existing reply does not change it. Once the seven-day active window passes, stale or deleted reply text can remain indexed and searchable until the thread's one-in-28-day refresh.
How this was verified: The listing hash excludes reply edit and deletion state, and a matching root version is classified as current without hydrating the thread.
Knowledge Base Used:
| return new Error(`${operation} failed (${reason ? `${code}, ${reason}` : code})`, { | ||
| cause: error, | ||
| }) |
There was a problem hiding this comment.
Redaction preserves sensitive cause
The sanitized error still attaches the original Drizzle error as its cause, even though that error carries the SQL and bound file text this helper is meant to redact. The indexing task throws this wrapper to Trigger.dev, so cause-chain serialization can retain confidential file content. Equivalent background database-error paths avoid this by throwing a cause-free diagnostic.
How this was verified: The wrapper directly retains the original parameter-bearing Drizzle error as its cause before the indexing task throws it.
| if (run === 0) runFillsLines = lineChars === 0 | ||
| run++ | ||
| lineChars++ | ||
| upper ||= isUpper | ||
| lower ||= isLower | ||
| digit ||= isDigit | ||
| } else if (code === 10) { | ||
| if (!(run > 0 && runFillsLines && lineChars >= FILE_SEARCH_ENCODED_WRAP_MIN_CHARS)) endRun() |
There was a problem hiding this comment.
Indented base64 bypasses detection
Wrapped base64 is joined across lines only when the run starts at column zero. Common indented 64- or 76-column payloads in YAML, generated source, and nested text are therefore split into runs shorter than 256 characters and report no encoded bytes. These base64-dominated files proceed into the expensive trigram index instead of being skipped. Please track wrapped runs independently of leading indentation and add coverage for an indented payload.
There was a problem hiding this comment.
4 issues found across 137 files
Confidence score: 2/5
apps/sim/lib/core/errors/database-query-error.tscan expose SQL and bound parameters through theDrizzleQueryErrorcause chain, allowing indexed file content to bypass redaction; omit the original query error ascause.apps/sim/lib/knowledge/search/tin-keyword.tsmay retain atruecache value for ten minutes after Tin rows are deleted, causingselectTinPageto treat empty pages as successful and return no matches; invalidate or refresh this state when an index is demoted.apps/sim/connectors/slack/slack.tscan treat a quiet thread as current fromrootVersioneven after an existing reply is edited or deleted, returning stale thread data; track reply edit/deletion state or force hydration before accepting the listing.apps/sim/lib/workspace-files/search/index-plan.tssplits indented wrapped payloads at every newline whenrunFillsLinesis false, so base64-heavy files may evade exclusion; track payload line width independently of indentation.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/sim/lib/knowledge/search/tin-keyword.ts">
<violation number="1" location="apps/sim/lib/knowledge/search/tin-keyword.ts:39">
P1: When a search index is demoted, this cache can keep `true` for ten minutes after the migration trigger deletes its Tin rows. `selectTinPage` treats `ranked = 0` as a successful page, so searches return no matches instead of falling back to GIN. Invalidate membership on `isSearchIndex` changes or avoid caching positive entries.</violation>
</file>
<file name="apps/sim/lib/core/errors/database-query-error.ts">
<violation number="1" location="apps/sim/lib/core/errors/database-query-error.ts:23">
P1: Do not attach the original query error as `cause` here. `DrizzleQueryError` retains SQL and bound parameters, so the indexing task can still expose file content through the supposedly redacted cause chain.</violation>
</file>
<file name="apps/sim/lib/workspace-files/search/index-plan.ts">
<violation number="1" location="apps/sim/lib/workspace-files/search/index-plan.ts:46">
P2: Indented wrapped payloads are split at every newline because `runFillsLines` is false after leading whitespace. Track payload line width independently of indentation so base64-dominated files are excluded from the expensive trigram index.</violation>
</file>
<file name="apps/sim/connectors/slack/slack.ts">
<violation number="1" location="apps/sim/connectors/slack/slack.ts:545">
P1: A quiet thread is marked current from `rootVersion` alone, but Slack does not change that value when an existing reply is edited or deleted. Track reply edit/deletion state or force hydration before accepting this listed hash, otherwise stale reply text remains searchable.</violation>
</file>
Tip: instead of fixing issues one by one fix them all with cubic
Re-trigger cubic
| /** Only organization search indexes are projected; `is_search_index` is fixed at creation. */ | ||
| const searchIndexBases = new LRUCache<string, boolean>({ | ||
| max: 10_000, | ||
| ttl: SEARCH_INDEX_TTL_MS, |
There was a problem hiding this comment.
P1: When a search index is demoted, this cache can keep true for ten minutes after the migration trigger deletes its Tin rows. selectTinPage treats ranked = 0 as a successful page, so searches return no matches instead of falling back to GIN. Invalidate membership on isSearchIndex changes or avoid caching positive entries.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/sim/lib/knowledge/search/tin-keyword.ts, line 39:
<comment>When a search index is demoted, this cache can keep `true` for ten minutes after the migration trigger deletes its Tin rows. `selectTinPage` treats `ranked = 0` as a successful page, so searches return no matches instead of falling back to GIN. Invalidate membership on `isSearchIndex` changes or avoid caching positive entries.</comment>
<file context>
@@ -0,0 +1,95 @@
+/** Only organization search indexes are projected; `is_search_index` is fixed at creation. */
+const searchIndexBases = new LRUCache<string, boolean>({
+ max: 10_000,
+ ttl: SEARCH_INDEX_TTL_MS,
+})
+
</file context>
| const code = getPostgresErrorCode(queryError) ?? 'no error code' | ||
| const reason = getPostgresCancellationReason(queryError) | ||
| return new Error(`${operation} failed (${reason ? `${code}, ${reason}` : code})`, { | ||
| cause: error, |
There was a problem hiding this comment.
P1: Do not attach the original query error as cause here. DrizzleQueryError retains SQL and bound parameters, so the indexing task can still expose file content through the supposedly redacted cause chain.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/sim/lib/core/errors/database-query-error.ts, line 23:
<comment>Do not attach the original query error as `cause` here. `DrizzleQueryError` retains SQL and bound parameters, so the indexing task can still expose file content through the supposedly redacted cause chain.</comment>
<file context>
@@ -8,3 +8,18 @@ import { DrizzleQueryError } from 'drizzle-orm/errors'
+ const code = getPostgresErrorCode(queryError) ?? 'no error code'
+ const reason = getPostgresCancellationReason(queryError)
+ return new Error(`${operation} failed (${reason ? `${code}, ${reason}` : code})`, {
+ cause: error,
+ })
+}
</file context>
| const next = parseContentHash(candidate) | ||
| const previous = parseContentHash(stored) | ||
| if (!next || previous?.kind !== 'hydrated') return 'stale' | ||
| if (next.kind === 'listed') return previous.version === next.version ? 'current' : 'stale' |
There was a problem hiding this comment.
P1: A quiet thread is marked current from rootVersion alone, but Slack does not change that value when an existing reply is edited or deleted. Track reply edit/deletion state or force hydration before accepting this listed hash, otherwise stale reply text remains searchable.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/sim/connectors/slack/slack.ts, line 545:
<comment>A quiet thread is marked current from `rootVersion` alone, but Slack does not change that value when an existing reply is edited or deleted. Track reply edit/deletion state or force hydration before accepting this listed hash, otherwise stale reply text remains searchable.</comment>
<file context>
@@ -403,7 +448,142 @@ function listingToken(syncContext?: Record<string, unknown>): string {
+ const next = parseContentHash(candidate)
+ const previous = parseContentHash(stored)
+ if (!next || previous?.kind !== 'hydrated') return 'stale'
+ if (next.kind === 'listed') return previous.version === next.version ? 'current' : 'stale'
+ return previous.text === next.text ? 'equivalent' : 'stale'
+}
</file context>
| const isLower = code >= 97 && code <= 122 | ||
| const isDigit = code >= 48 && code <= 57 | ||
| if (isUpper || isLower || isDigit || code === 43 || code === 47) { | ||
| if (run === 0) runFillsLines = lineChars === 0 |
There was a problem hiding this comment.
P2: Indented wrapped payloads are split at every newline because runFillsLines is false after leading whitespace. Track payload line width independently of indentation so base64-dominated files are excluded from the expensive trigram index.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/sim/lib/workspace-files/search/index-plan.ts, line 46:
<comment>Indented wrapped payloads are split at every newline because `runFillsLines` is false after leading whitespace. Track payload line width independently of indentation so base64-dominated files are excluded from the expensive trigram index.</comment>
<file context>
@@ -2,11 +2,79 @@ import { Buffer } from 'node:buffer'
+ const isLower = code >= 97 && code <= 122
+ const isDigit = code >= 48 && code <= 57
+ if (isUpper || isLower || isDigit || code === 43 || code === 47) {
+ if (run === 0) runFillsLines = lineChars === 0
+ run++
+ lineChars++
</file context>
* fix(files): bound file version retention per run and document both coalescing cutoffs * fix(files): agree on the current version when a write skipped recording, and tidy CLI version output * fix(files): size each retention batch to the remaining run allowance * fix(files): read file versions against one snapshot of the file
* fix(slack-search): reuse manifest installation in setup * fix(slack-search): register token connection in operation test
* refactor(ui): reuse EMCN icons for simple product glyphs * docs(emcn): add CircleStop usage example --------- Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
…8019) * feat(google-calendar): add RSVP operation to respond to invitations * fix(google-calendar): honor cancellation on RSVP write and tighten outputs * fix(google-calendar): verify the RSVP note was saved
Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
* fix(realtime): validate cursor and selection presence payloads The workflow cursor-update and selection-update handlers stored whatever object a client sent into the shared room presence hash and rebroadcast it to every peer, with no shape or size check. An authenticated user with read access to any workflow could park a multi-megabyte blob in shared Redis on every socket they opened and have the server fan it out on each presence broadcast. Both payloads are now rebuilt from a fixed field set before they reach room state or any broadcast, so unexpected keys cannot ride along - mirroring normalizeCellSelection in the table presence handler. Adds a defensive per-field length cap in updateUserActivity so future presence-bearing events inherit the bound, and marks UserPresence.cursor nullable to match the cleared-cursor value the client already sends. * fix(realtime): measure presence field cap in utf-8 bytes The cap compared UTF-16 code units against a byte budget, so a multi-byte payload could pass the check and still land several times larger in the room hash. It now measures the UTF-8 bytes Redis actually stores. Raises the ceiling to 16384. A table cell selection carries four ids capped at 200 characters each, and multi-byte characters plus JSON escaping can expand a legitimate worst case to roughly 5 KB - above the previous 4096, so the old bound could have dropped real presence.
…d tool route (#8042) Tool results reach the model over two lanes, but the model-facing projection was applied on only one. The resume lane runs results through getToolCallTerminalData, which reduces generate_api_key to its status message. The in-band route (POST /api/copilot/tools/execute) returned the handler output verbatim, so the freshly minted plaintext workspace API key crossed to the model and into the turn transcript. The egress projection cannot cover this: its registry is a catalog of pre-existing environment and credential secrets, built once per turn, so a key minted mid-turn is invisible to it. Apply toolResultForModel at the route so both lanes return the same model-facing projection. It is an identity for every other tool.
* feat(memory): preserve durable Agent tool history * docs(agent): explain durable tool history and context limits * feat(memory): bound durable context and retrieve retained tool detail * fix(memory): validate checkpoint recovery and bounded summary coverage * chore(db): format durable memory migration metadata * docs(memory): remove redundant internal README * fix(memory): preserve tool loops and harden durable history * fix(memory): preserve stream usage and bound portable history * fix(memory): surface bounded history and validate replay inputs * fix(memory): retain legacy function call exchanges * fix(memory): admit only complete stored tool exchanges
| JSON.stringify({ | ||
| providerId, | ||
| model: request.model, | ||
| endpoint: | ||
| request.azureEndpoint ?? | ||
| (providerId === 'azure-openai' ? env.AZURE_OPENAI_ENDPOINT : undefined) ?? | ||
| (providerId === 'vllm' | ||
| ? env.VLLM_BASE_URL | ||
| : providerId === 'litellm' | ||
| ? env.LITELLM_BASE_URL | ||
| : providerId === 'ollama' | ||
| ? getOllamaUrl() | ||
| : undefined), | ||
| apiVersion: | ||
| request.azureApiVersion ?? | ||
| (providerId === 'azure-openai' ? env.AZURE_OPENAI_API_VERSION : undefined), | ||
| systemPrompt: request.systemPrompt, | ||
| systemMessages: request.messages?.filter((message) => message.role === 'system'), | ||
| context: request.context, | ||
| account: { | ||
| apiKey: request.apiKey, | ||
| accessKey: request.bedrockAccessKeyId, | ||
| secretKey: request.bedrockSecretKey, | ||
| }, | ||
| project: request.vertexProject, | ||
| location: request.vertexLocation, | ||
| region: request.bedrockRegion, | ||
| tools: request.tools?.map(getConfiguredConversationToolBinding), | ||
| responseFormat: request.responseFormat, | ||
| reasoningEffort: request.reasoningEffort, | ||
| thinkingLevel: request.thinkingLevel, | ||
| }) |
…#8044) * improvement(copilot): refuse approval-gated tools on the in-band lane Copilot's approval gate is scaffolding today: COPILOT_TOOL_PERMISSIONS_ENABLED is off by default, so nothing is gated on any lane. It is built only on the dispatch lane, which holds a call against a streaming context and a decision row and then declines to dispatch anything the mothership marks in-band. Those calls run via POST /api/copilot/tools/execute, which has no context and no waiter, so turning the flag on would gate the foreground and leave background lanes ungated — a gate that looks enforced but is not. Add toolRequiresApprovalLane next to toolCallNeedsApproval so the covered tool set is defined once, and refuse a gated tool at the in-band route before it runs. Refuse rather than block: a background lane must never hang on a prompt with no row behind it. The check deliberately ignores the stored auto-allow list — an auto-allowed tool sent to the checkpoint lane is admitted there without prompting anyone, so reading it here would only add a database read to reach the same place. Inert while the flag is off, which is the state this ships in; a test pins that. Also record on the flag itself that the gate is a property of the lane, since that is what the next person reads before enabling it. * improvement(copilot): move the approval-lane predicate beside the tool router Importing the dispatch gate module for a one-line predicate pulled the permission persistence layer in with it, whose module body opens a pub/sub channel — two Redis clients and a channel subscription — in every process that loads the in-band route. Move toolRequiresApprovalLane to tool-executor/router.ts, which imports only the catalog. The route already imported @/lib/copilot/tool-executor for ensureHandlersRegistered, so the guard now costs no new import edge at all. The dispatch gate keeps a pointer to it. Its flag-and-catalog behavior is covered in the router tests against the real flag and the real catalog; the route tests keep to what the route does with the answer.
Extend the Confluence attachment allowlist so .pptx and .xlsx files on synced pages and blog posts are listed and handed to the shared parser pipeline the same way PDF and Word attachments already are. Macro-enabled, template, legacy binary and OpenDocument variants stay excluded. Add listing, hydration, genuine-bytes roundtrip and renamed-to-unsupported coverage, and update the connector guides to name the new formats.
* fix(ui): align fallback model subblock styling * fix(ui): keep editor pickers on shared combobox styling
Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
* improvement(ui): reuse chip inputs for remaining settings fields * improvement(ui): reuse the shared branding upload drop zone (#8031) Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local> --------- Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
…ke (#8052) * improvement(utils): add escapeRegExp and compareStrings to @sim/utils/string * improvement(utils): replace nine local escapeRegExp copies with the shared helper * improvement(utils): adopt shared compareStrings and isRecordLike over local copies * docs: document the shared escapeRegExp, compareStrings, and isRecordLike helpers * improvement(utils): finish the escapeRegExp sweep and share the metacharacter class Replaces the nine inline copies of the escape body the name-based sweep missed, folds the catalog cursor comparator into compareStrings, and gives linear-regex its metacharacter test from the same source the escaper uses.
…ord coercions (#8053) * improvement(utils): adopt toRecord and toRecordOrNull over inline record coercions * improvement(utils): route the remaining record-coercion helpers through toRecord The first pass matched one operand order, so six exact equivalents written `x !== null && typeof x === 'object'` survived — two of them beside a sibling the pass had already deleted. Domain-named wrappers keep their names and delegate, matching the microsoft-teams client that already did.
* refactor(ui): share log and enrichment details panels * fix(ui): prevent focus in closed details panels * refactor(ui): use CSS-variable sizing for details panels * style(ui): make resize calculation spacing explicit --------- Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
Uh oh!
There was an error while loading. Please reload this page.