v0.8.0: desktop app, workflow UI, v2 endpoints - #6606
Conversation
…6513) * fix(provenance): name every guard that can latch a registry A production latch reported `reason: "unspecified"` because 44 call sites took the default. The reason is the only thing that names which guard tripped, and a refusal surfaces many frames later as one fixed sentence, so an unnamed latch is undiagnosable — that is what left an incident's origin unidentified for a day. Give each call site a literal that names its guard, add the 20 new literals to the reason union, and sort them into the existing error/warn split: a guard that should not trip on a healthy run reports at error, everything else stays at warn. `log-creation-skipped` joins the by-design set since it fires on every run that does not persist a log. Make `reason` required on both `markIncomplete` and `markInputPathIncomplete`, so omission is a compile error rather than a silent `unspecified`. A caller with genuinely nothing to say now passes `'unspecified'` where a reviewer can see it. The three remaining bare calls are on ResolvedSecretTraceProvenanceAccumulator, a different class with no reason concept. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(provenance): pin the new reasons and the guard that latched in production Cover what the reason set is for rather than only that it compiles: the non-enumerable tool-params branch now asserts it names `tool-input-not-enumerable`, which is the guard the production logs showed reporting `unspecified`, and every new literal asserts which stream it reports on — error for a guard that cannot trip on a healthy run, warn for one reachable without a fault, silent for the by-design log-less session. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor(provenance): make reason the only locator, not one of two Two fields had grown into competing answers to the same question. `origin` is a free-form label for which importer accepted an already-incomplete bundle; four latches had started passing `markIncomplete('unspecified', { origin })`, using it to stand in for a reason that did not exist yet. That splits one fact across a closed enum and an open string, leaving neither worth alerting on. Give those four the literal they were reaching for — none needed a new one — and split the five reasons that covered genuinely different guards, so the reason alone locates the site rather than needing an origin beside it. `origin` keeps its narrow job, now documented: it disambiguates importers that share one guard, and a latch that wants an origin because no reason fits should add a reason instead. No production call site passes 'unspecified' any more. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(provenance): stop two expected states from reporting as faults Cursor Bugbot caught `backfill-checkpoint-*`: the guard covered four conditions under one reason classified as an originating fault, and one of them — a state persisted before the checkpoint contract existed — is what essentially every legacy row looks like. A backfill over historical rows would have put one error line per row into the stream the error/warn split exists to protect. Auditing the rest of the error-level reasons for the same shape found a second: a client tool invoked without a run id has no binding to unseal against, so it took the `[null, null]` path and reported `client-tool-seal-failed` at error on an ordinary configuration. Split both along the line that matters — absent versus unusable, not attempted versus failed — and classify each half: expected states warn, genuine faults keep error. `backfill-scope-mismatch` is retired; it named one of its four conditions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(comparisons): add code-sandbox and session-policy rows Add two comparison rows covering configurable code sandboxes and admin-configurable session policy, populated for Sim and all 20 competitor profiles. Source Sim's facts from the docs rather than the codebase, and repoint the execution-limits row at the Run Time Limits table. * fix(comparisons): resolve customCodeSteps contradictions for Retool and Workato Both profiles documented code steps in the new codeSandboxRuntime row while their adjacent customCodeSteps row still read Unknown/unclear, so the rendered table gave contradictory answers for the same capability. Settle both from the vendor docs already cited by the sandbox row.
…rs (#6515) * fix(folders): show the folder trail in table and knowledge base headers A table or knowledge base opened from inside a folder rendered `Tables / name`, dropping every folder between it and the root — while a file's header showed the full `Files / docs / name` path. The detail pages never read the resource's own `folderId`, so the trail could not include it. Converge all six foldered surfaces on one builder instead of fixing the two headers in place: - `folderAncestorChain(folderId, lookup)` in `lib/folders/tree.ts` is now the single upward walk. Both `getFolderPath` variants delegate to it; the two lock predicates deliberately keep their inline walks, which short-circuit at the first locked ancestor on a per-row render path. - `folderBreadcrumbItems` takes a `trailing` slot for detail pages, as a discriminated union so an open-folder rename cannot be passed alongside it and silently dropped. - `useFolderAncestors` owns the tree plus the `foldersResolved` staleness rule; `useFolderNavigation` now delegates to it. - `FOLDERED_RESOURCE_HEADERS` owns each resource's root label, root icon, and list path, which seven sites previously restated. Files' list trail moved off splitting the materialized `path` string, which could not tell two same-named siblings apart, onto the shared parentId walk. Also fixes the Files loading trail, whose folder crumbs used the nuqs setter while rendering on the file detail route — appending `?folderId=` to the open file's own URL instead of navigating to the list. * fix(knowledge): confirm before a breadcrumb navigates away from an unsaved chunk * fix(knowledge): source the pluralized root label from the folder registry * chore(folders): tighten the shared breadcrumb docs and dedupe the ancestry type
… dsl for natural language preview, retry configs (#6458) * improvement(workflow): refine canvas interactions and rendering * fix(workflow): keep outputs on the right, focus newly created blocks Connection anchors: an output now always leaves a card from the right. The cursor swell lets a drag start on any edge, but the left side is the input, so anchoring an outgoing edge there drew a line out of the input port and read as a second input. `normalizeCursorSourceHandleId` resolves every drag to the right anchor, `normalizePositionedSourceHandleId` collapses `source-left` alongside the legacy vertical anchors (so data from the API, an older client, or a stale save self-heals on load), and only the right-side source anchor is mounted. Drops in `onConnectEnd` are always source -> target. The branch that reversed the edge for a drag starting on an input could never run: the `target` handle is `isConnectableStart={false}` and the positioned side anchors are `isConnectable={false}`, so React Flow never reports an input as a drag origin. Removed it and its now-unused imports. A newly created block is centered once its node mounts and is measured, so a card added from a drag-release, the block menu, or the toolbar is never left off-screen or under the editor panel. The editor panel's block icon uses the same type accent as the card's badge instead of the block's legacy `bgColor`, which had left the panel on the old per-integration brand colours. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(workflow): floor header-only card height, adopt brand tag palette The Start card intermittently collapsed after load, squashing the action-menu tab so its icon row sat over the card. `.workflow-drag-handle` is the host the border renderer measures, and both it and the header row took their height from `blockHeight && blockHeight > 0`. `blockHeight` comes from the deterministic-dimensions pass and is already floored at MIN_PAINTED_HEIGHT (48), but it is absent on the first frames — and with no floor the host collapsed to its natural content height (25.5px for a header-only trigger, exactly the title's line box). The border builds its perimeter from `host.offsetHeight`, so that window painted a sub-floor card: too little straight edge remained on the vertical runs for the action-menu tab, which collapsed into the corner arcs. Whether you saw it depended purely on whether the dimension publish had landed, which is why it reproduced on one workflow and not another. Floor all three: the host, the header row (so `items-center` centres the title and type tag rather than pinning them to the top), and the border's own `offsetHeight` read. Also raise ACTION_MENU_CONTENT_READY_THRESHOLD to 0.9. At 0.8 the 24px icon row was revealed while the swell had only reached 22.4px of its 28px — shorter than the row it contains. Secondary to the above, but a real overflow window on its own. The test now pins the ratio rather than the constant. Tag palette moves to fixed brand values (hex, not derived oklch) with two inks — #F8F8F8 on dark fills, #1A1A1A on light. Tones are renamed to match what they render. `green` (2.55:1) and `orange` (3.15:1) sit under WCAG AA against their paired ink; both are deliberate brand decisions and are documented in the component. Deploy and Run take two new Button variants rather than className overrides, so `tertiary` stays green everywhere else. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * improvement(workflow): polish workflow canvas interactions * fix(workflow): restyle loop drop target outline * fix(workflow): shorten human block catalog label * fix(workflow): canonicalize realtime edge handles * code cleanup * sizing fixes * improvement(blocks): sentencify every block * change tebse * improvement(workflow): land notes UI and execution progress, consolidate duplicates Ports the notes canvas editing and execution progress-indicator work, then removes the parallel paths it arrived with so each concern has one owner. Fixes found while consolidating: - Note height was measured while the card was expanded to NOTE_EXPANDED_WIDTH, where text re-wraps shorter, and published as the node's compact height. The collapse then animated to a height never measured at the compact width. - The edge pulse glow filter used the default objectBoundingBox units, so a straight horizontal edge — what an auto-laid-out chain produces — resolved the filter region to zero height and stopped the glow rendering entirely. - A subflow's inner Start pill still read isNodeSelected while its border read usesSelectedVisuals, so the two disagreed during execution. - The Run/Stop button's disabled prop gated only Run while its handler cancelled unconditionally, offering a Stop the cancel route answers with 403. Consolidated: - One note editor. The view's built-in textarea was unreachable in production (the app always injects the markdown editor) and was kept alive only by tests asserting against it; renderContentEditor is now required. - onBlur/onCancel collapse to onEndEditing — content persists per keystroke, so there was never a draft for a cancel path to discard. - DEFAULT_NOTE_COLOR, the note height bounds, the note content reader and the card width each had two or three definitions; each now has one. - Removed with zero consumers: graphite/graphiteSubtle button variants, data-subflow-selected, inputPlaceholderClassName, an effect that could never fire, and getNoteColorOption's unreachable fallbacks. Restores the role='status' announcement the progress rewrite dropped, and hardens isNoteColor against inherited Object keys. Co-Authored-By: Claude <noreply@anthropic.com> * improvement(workflow): reuse the platform markdown editor in notes Notes carried their own TipTap wiring — a second markdown editor that reimplemented, more thinly, what `RichMarkdownField` already does for the skill modal, skill fields and the deploy version description. It is now a ~20 line skin: the Note supplies its type scale and per-colour selection tint, and the field supplies the extension set, frontmatter held out-of-band, the round-trip safety gate and its raw-source fallback, and markdown paste. `RichMarkdownField` gains two additive props, both defaulting to today's behaviour so the file editor is untouched: `surface` ('field' | 'bare') and `proseClassName`. All three existing consumers pass an explicit `minHeight` and no `surface`, so they take the original path unchanged. Exiting the note editor moved to the card, because the editor's `/` and `@` menus consume Escape to close themselves and ProseMirror checks `editorProps` before plugin handlers — intercepting it inside the editor would have broken both menus. The card now honours Escape only when nothing already consumed it, which also let `onEndEditing` leave the injection contract. The note editor is lazy now, matching every other consumer: it was pulling TipTap and the full extension set into the canvas's initial chunk. Also: - One `areRunFromBlockDependenciesSatisfied`. The ActionBar, the canvas context menu and the run-from-block handler each carried a byte-identical copy, and the handler expressed the snapshot requirement differently, so the affordance and the action could disagree. Each copy also re-scanned `edges` once per incoming edge, on every ActionBar on the canvas. - Reduced motion is one `usePrefersReducedMotion` in @sim/emcn rather than a sixth ad-hoc `matchMedia`. The edge pulse now stops rendering instead of hiding: `motion-reduce:hidden` is `display: none`, which left four SMIL timelines running per edge. - The pulse glow bleed covers the canvas minimum zoom. The strokes are `non-scaling-stroke`, so the 6px tail spans 3/zoom user units — 30 at 0.1. Co-Authored-By: Claude <noreply@anthropic.com> * improvement(workflow): port canvas styling from workflow-updates Ports the 14 styling commits your colleague added since the last sync, leaving the ~68 staging PRs on that branch alone — those are platform/core work, not this. Cherry-picked individually rather than merged so each conflict was small enough to reason about. What came in: - Core block colors unify behind a two-level map: block type -> semantic role -> accent, replacing the flat per-type table. Adds `purple` and `content` tones to ChipTag, and a shared `WorkflowTypeIcon` that replaces the hand-rolled ChipTag + accent lookup at each discovery surface. - Native triggers take semantic colors; the deployments block moves to the shared Rocket icon and drops its now-unused `iconColor`. - Running-state polish: loader artwork and position, stop hover in dark mode, the loader blended into the execution swell, and tooltips suppressed for actions that are hidden mid-run. - The toolbar drag preview clones the rendered icon container instead of rebuilding a bgColor tile, so it matches what the canvas paints. - The sidebar shows route-derived workspace identity instead of a skeleton while the full record loads. Conflict resolutions worth knowing: - The running-loader artwork went through the shared `Loader` and back to the custom SVG on their branch; the second commit is the intent, so that is what landed — keeping our `role='status'` announcement layered on top. - Two commits carried the lucide-react -> in-house icon migration along with them. That migration is a staging change we have not taken, so our imports stayed on lucide: adopting it in two files would leave the icon set split across the app. - `getMappedWorkflowTypeAccent` referenced a constant their refactor removed. It had no consumers left once the search modal moved to `WorkflowTypeIcon`, and their branch deletes it too, so it is gone here. Co-Authored-By: Claude <noreply@anthropic.com> * feat(workflow): make the subflow Start swell a real connection source Dragging an edge into whitespace opens the add-block picker, but starting that drag from a loop/parallel Start pill did nothing: the pill's border swell was visual-only. Regular blocks and the container's own exit mint a draggable cursor handle from their swell; the pill rendered only its invisible 14px static strip, so grabbing the glowing affordance started no connection at all. Everything downstream already worked and was nearly unreachable: - the drop hit-test skips subflow containers, so a release inside the loop opens the picker there (z 2000, above containers) - handleToolbarDrop parents the new block into the container at the drop point - it already carries the exact boundary rule for this source: a container start handle only wires to a child of that container The pill now runs the same cursor-handle machinery as the container view, with one deliberate difference: its temporary handle carries the branch-cursor form of the start id. The plain cursor id normalizes by block type — for a container that is `loop-end-source`/`parallel-end-source`, the exit — so a swell drag from Start would have persisted as an edge leaving the container. The branch form passes `loop-start-source`/`parallel-start-source` through normalization verbatim on both the picker and direct-connect paths; a test pins that contract. Co-Authored-By: Claude <noreply@anthropic.com> * fix(notes): stop the field's prose classes recoloring bare-surface editing Opening a note for editing shifted the text and turned it black: the ProseMirror root unconditionally carried `rich-markdown-prose rich-markdown-field-prose`, which pin the field's own ink and type ramp — `--text-primary` at 15px/25px, then 14px/22px — overriding the card's `text-current` at 14px/20px the moment the editor mounted. `surface='bare'` means the host owns typography (the Note card mirrors its rendered view via `proseClassName`), so on that surface the root now carries no shared prose classes. The field surface is untouched. Edit mode inherits the note colour's ink — including the caret — and sits on the same metrics as the read view. Co-Authored-By: Claude <noreply@anthropic.com> * fix(workflow): give the in-flight connection line contrast inside containers The drag line was drawn but camouflaged: its default stroke was the resting-edge grey (#e0e0e0), which disappears against a loop body's opaque `--surface-3` fill (~1.1:1) — so dragging an edge inside any container, nested included, showed nothing. The z-order was never the problem; the connection line layer already sits above every node. The default token is now `--text-muted`, one value with contrast on every canvas surface, still lighter than the `selected` variant so the variant hierarchy holds. No per-surface special-casing. Resting edges inside containers share the same camouflage (`--workflow-edge` on `--surface-3`) — left alone deliberately: recoloring placed edges is a design decision, not a bug fix. Co-Authored-By: Claude <noreply@anthropic.com> * fix(notes): match edit mode to the read view, and land the caret where clicked Three defects, all from the read and edit views being built independently. 1. Blocks jumped up ~12px on entering edit mode. Streamdown wraps its output in a container carrying `space-y-4` plus first/last margin resets, which outrank the per-element margins in NOTE_COMPONENTS — so that wrapper, not those margins, is what the read view actually paints. The editor had no equivalent. The rhythm is now named (NOTE_MARKDOWN_FLOW), passed to Streamdown explicitly so a dependency upgrade cannot move the read view out from under the editor, and mirrored on the ProseMirror root. Tailwind's JIT only sees literal strings so the mirror cannot be composed from the constant; a test pins the two together instead, and fails if either side drifts. 2. The caret was barely visible: it inherited the note's 75%-opacity ink. The palette owns per-colour chrome, so it now names the caret alongside the selection tint. 3. The caret always landed at the document end. The read view sits under a full-bleed overlay that must swallow the click to enter editing, so the point never reached the editor and `autofocus: 'end'` was all that was left. The view now forwards that point and the field resolves it through `posAtCoords` on create — after the DOM is laid out, which `autofocus` cannot wait for. Keyboard activation carries no point and still lands at the end. `autoFocusAt` is additive on the shared field and defaults to null, so the file editor and the other three consumers are unchanged. Co-Authored-By: Claude <noreply@anthropic.com> * fix(workflow): stop edges rendering behind top-level subflows Containers are z-indexed by nesting depth, so a top-level subflow is 0. Edges derived their z from their parent container — `+1`, or 0 with no parent — so a root-level edge landed on exactly the same z as a root-level subflow. Equal z-index falls back to DOM order, and React Flow paints the nodes layer after the edges layer, so the container's opaque body won: any edge crossing a top-level loop or parallel was drawn behind it, in-flight or persisted. Edges now sit in their own band above the whole container scale and below cards, keeping both the deeper-container-wins ordering and the rule that a line always passes behind card chrome. This is why the edge became visible only once a block was dropped: the new block is selected, and an edge inside the container was already `containerZ + 1`, clear of the tie. The in-flight connection line is declared in the same scale rather than inheriting React Flow's stylesheet default of 1001, which is both below a selected container child and outside the scale this file owns. Its stroke moves to `--text-secondary`, the token the canvas already uses for an active edge — the previous `--workflow-edge` grey is ~1.1:1 against a subflow body. Co-Authored-By: Claude <noreply@anthropic.com> * feat(notes): paste and drop images through the workspace-file pipeline Traced the file editor's image path end to end and reused it verbatim rather than minting a note-specific source: insertImages -> useUploadWorkspaceFile -> POST /api/workspaces/{id}/files/presigned -> direct-to-S3 PUT -> workspace_files row -> the editor persists the workspace-scoped /api/workspaces/{id}/files/inline URL, which the serve route authorizes by workspace membership and the embedded-image-ref machinery already recognizes for share rewriting and referenced-by-doc tracking. The shared field gains an optional `uploadImage(file) -> {url, alt} | null` capability. With it, image paste/drop uploads sequentially and inserts each at the evolving position, mirroring the file editor's flow, with a bail if the editor unmounts mid-upload; without it, the existing swallow-guard on file drops is unchanged, so the skill modal, skill fields and version-description consumers behave exactly as before. The upload mutation owns its own toasts. The note host wires the capability with folderId null, so note images land in the workspace Files root — visible, manageable and deletable there like any other upload. The note read view renders images through its Streamdown components map with the card's own sizing. Co-Authored-By: Claude <noreply@anthropic.com> * improvement for notes, subflows * fix(uploads): surface the server's message when a multipart upload is refused A file over the 50MB direct-PUT threshold goes through multipart initiate, which is where the storage quota is enforced — but the client threw away the response body and reported `Failed to initiate multipart upload: Payload Too Large`. That is the string the upload mutation puts in its toast, and it names neither which limit was hit nor by how much, so the one place that answer surfaces didn't have it. It now prefers `errorBody.error` exactly as `getPresignedUploadInfo` already does on the single-PUT path, and passes the body through as the error's details. Control flow is unchanged: still throws, still `MULTIPART_ERROR`, and the cloud-storage-absent branch above still claims its 400 first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(notes): restore GFM in the note read view Streamdown's `remarkPlugins` prop REPLACES its default plugin list rather than extending it, and remark-gfm is one of those defaults. The note passed `[remarkBreaks]` — so the read view silently lost every GFM construct: task lists, tables, strikethrough and autolinks. The editor writes all of them (it has TaskList, TableKit and Strike), so a note round-tripped through editing came back as raw source the moment editing closed: `- [x] HELLO` rendered as a disc bullet followed by the literal text `[x] HELLO`. `NOTE_COMPONENTS` has carried table/thead/tbody/tr/th/td entries this whole time that could never fire. Restoring the plugin is only half of it: remark-gfm marks a checklist `contains-task-list` and emits a native checkbox, which under the note's generic `ul` styling renders a checkbox sitting behind a disc bullet — the same defect the editor had before the chrome/typography split. The read view now drops the marker and indent for a task list, lays the row out as a flex line, and styles the checkbox to match `.rich-markdown-nodes input[type="checkbox"]` declaration for declaration, tick clip-path included, so the two views agree either side of a click. `remark-gfm` is now a declared dependency of the renderer package rather than one borrowed transitively from streamdown. Five tests cover the GFM surface — checked/unchecked boxes, no literal `[x]`, the marker only dropped for checklists, tables, strikethrough — and four go red with the plugin removed. Checked the other three Streamdown call sites (Chat, the chat interface renderer, the changelog): none override `remarkPlugins`, so none were affected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor(canvas): drop dead markers and share the tile-brightness maths Review pass over the branch against staging. Dead code removed: - `tileIconColorClass` in the renderer package — never called; only its `isLightTileColor` sibling is. - `data-connection-selector-search-frost`, `data-workflow-cursor-edge` and `data-workflow-cursor-source-side` — written on three elements, read by no stylesheet, selector or test. - `CHIP_TARGET_SELECTOR_TYPES`, `MAX_CHIPS` and `chipPriority` were exported from `canvas-rows.ts` but only used inside it. Consolidated the one real divergence: the renderer package carried a hand-copied mirror of the app's perceived-brightness maths, because it may not import app code. The copy had already drifted — it dropped the `white`/`black` keyword handling, so a block shipping `bgColor: 'white'` would render a white `currentColor` icon on a white tile on the canvas while every other surface drew it black. No block ships one today, which is exactly why nothing caught it. The function now lives in `@sim/utils/color` and both sides import it; only the 0.75 threshold stays local to each. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor(canvas): share the z-scale, fix the preview's edge layering, and re-home strays The preview canvas carried its own z numbers and had the collision the editor canvas was fixed for: containers at nesting depth, top-level cards at an implicit 0, and edges at 0/5/10 by execution status — so a default edge tied with a top-level subflow and painted behind it, while a success edge painted over unselected cards. The scale now lives once, in `@sim/workflow-renderer/canvas-layers`, and both canvases read it. The preview keeps its status ordering, expressed inside the shared edge band rather than as a second set of magic numbers. Placement and duplication: - `perceivedBrightness` moved to `@sim/utils/color`, with its unit test, and its consumers import it directly. It had been re-exported through `lib/colors/brightness.ts`, and the renderer package kept a hand-copy. - `filterAcyclicEdges`/`wouldCreateCycle` were pass-through wrappers in the workflow store's utils over the real implementations in `@sim/workflow-types`. Deleted; the three consumers import the source. - `lib/ui/glass-surface.ts` was a one-constant, one-consumer app-wide module, and its consumer then aliased it a second time. Collapsed into the navbar shell. - `nested-subflow-node` was set on nested container nodes in both canvases with no stylesheet, selector or test behind it. `packages/workflow-renderer` now has its own vitest config, so the four mount tests for its components live with the components instead of in `apps/sim/lib/workflows/**`. That immediately earned its keep: `apps/sim` excludes test files from type-check, and once these were checked, tsc found three `SubflowNodeView` renders being handed a `renderContentEditor` prop it does not accept — a copy-paste from the note cases that had been silently ignored. Verified: type-check 23/23, 21,143 app tests + 49 renderer + 147 utils, biome clean, all 23 audits pass (`check:bare-icons` imported the moved helper and was repointed). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix field noun bug + notes * fix(notes): let a note service the canvas actions the panel editor cannot The panel editor clears any note put in front of it and renders nothing, but the block menu still routed Rename and Open Editor through it. Rename latched the editor's rename state onto the note — `handleStartRename` reads the store directly, so it saw the id the menu had just set — and nothing reset it when the clear ran. `handleSaveRename` writes to `renamingBlockIdRef`, so the header went on showing a rename field over whatever was selected next and saved that name to the note. Open Editor was a plain no-op that opened an empty pane. Rename now goes to the card, which expands and opens its own title — the same menu-to-card routing Add Image already used, so both events now live in one `lib/workflows/notes/canvas-requests.ts` and `add-image.ts` keeps only its markdown concern. Open Editor is hidden for notes. The panel editor also drops any rename whose block stops being the selected one. That is belt-and-braces for notes now, but it closes the same hole for ordinary blocks, where only the input's blur ended a rename and blur only fires if it held focus. A rename interrupted that way is now discarded rather than left pending. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(canvas): author sentences for Snowflake and Mintlify, repoint Instagram's Staging's two new integrations shipped without a `canvasPresentation`, so their 39 operations painted the field rows the rest of the canvas has stopped using. The Instagram break is the more interesting one: staging renamed the insight metrics subblock `metrics` -> `insightMetrics` while this branch was adding sentences that named `metrics`. Both hunks merged cleanly — the union check reports the file as an exact union — and the result was two clauses pointing at a field that no longer exists, which resolves to nothing with no error and no log. Only `check:canvas-sentences` sees it. Two Snowflake sentences say something the block does not do, so they anchor elsewhere: `taskName` filters `list_task_runs`/`get_task_run` rather than keying them, and `table` filters `introspect_schema` — blank means "every one", not "not filled in yet", and a core chip would have claimed otherwise. Coverage is back to 4727/4727 operations across 321/321 blocks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(canvas): keep the block-type tag naming its type after a rename The header tag dropped its label whenever the block's title already said the same word, so the same block read two ways depending on nothing the user did deliberately: a freshly dropped Wait showed a bare icon, and its second copy — auto-named "Wait 2" — showed "Wait". The tag looked like a badge that appeared on rename rather than a fixed part of the header. It now always names the type, which is what loop and parallel containers already do with their own tag, so every card on the canvas reads the same way. `blockName` was only ever read for that comparison, so the prop is gone rather than left behind for a future reader to wonder about. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(deploy): compare edge handles by port, not by spelling Two places answer "does this need redeploying?" and they load their sides differently. The client diffs the live store against `/api/workflows/[id]/deployed`; the server diffs the normalized tables against the version's raw jsonb. Only some of those paths run handles through `loadWorkflowFromNormalizedTables`, so a snapshot holding a side-anchored id (`source-right`) met a canonical one (`source`) on the other side and the set comparison read it as every edge being removed and re-added. Each answer therefore differed, and they arrive on separate query timelines: the button reads the client's, the modal badge reads the server's, so the state flipped between Live and "Update deployment" with whichever query landed last until both settled. `normalizeEdge` now canonicalizes both handles, so the comparison cannot tell two spellings of one port apart no matter how its inputs were loaded. The existing normalization in `materializeDeploymentState` stays — that path also feeds React Flow, which needs the handle it mounts to match. The preview's error port had the mirror problem: it rendered for every non-trigger block regardless of `errorEnabled`, so a card with no error row grew a red knob anyway. It now gates the way the editor canvas does, keeping the port mounted when an error edge already leaves it so React Flow cannot drop that edge. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(deploy): stop counting the error flag twice in change detection `errorEnabled` has two homes. It persists inside the block's `data` jsonb — the realtime server `jsonb_set`s it there, and load mirrors it back onto the block as a field — so it reached the diff twice, and only some paths populate the copy. `setBlockErrorEnabled` writes the mirror alone, so right after toggling the port the live block said `errorEnabled: true` with `data.errorEnabled: false`, while the snapshot the deploy had just taken from the tables said true in both. The diff read the stale `data` and reported the workflow as changed the instant it finished deploying — then a state refetch rehydrated the block and it agreed again. That is the flip between Live and "Update deployment": the button and the modal read two different queries, so each landing swapped the answer. A block created in-session had the same shape from the other side, its `data` carrying no key at all against a persisted `false`. Excluded from `normalizeBlockData` alongside the other fields that are duplicated out of the block's own state. The block field is still compared on its own, with `!!`, so absent and `false` agree and turning the flag on is still a change. Fixing the store to write both homes was the other option and is not taken: nothing reads the in-memory `data.errorEnabled` (save and load both let the block field win), so it would add a second copy that only the diff could see — which is the shape of this bug, not its fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor(blocks): give the error-output flag a column instead of two homes `errorEnabled` had no column, so it persisted inside the block's `data` jsonb and was mirrored onto the block as a field on load. Every writer had to route its `data` through `withPersistedErrorEnabled` or silently drop the toggle, the realtime op `jsonb_set`, and change detection saw the same value twice — which is what made the deploy badge flip between Live and "Update deployment" after toggling the port. Its siblings — `enabled`, `horizontal_handles`, `advanced_mode`, `trigger_mode`, `locked` — are all boolean columns; `data` is for React Flow and subflow state. The flag belongs with them, so it now has `error_enabled` and one home. The shuttle helper, its `BlockData` mirror, the store's fallback read, and the comparison exclusion the duplication forced are all gone. Backwards compatibility, since released versions draw the error port with no toggle in front of it: a block already wired to an error edge HAS the output on, because there was no other way to draw that edge. That rule is now stated in three places and none may be narrowed to read the flag alone — - the migration backfills `error_enabled` from the edges, so live rows are true before any new code reads them; - `materializeDeploymentState` derives it for a version's frozen jsonb, which the migration cannot reach — otherwise every workflow deployed before the toggle would ask to be redeployed once; - `workflow-block.tsx` keeps it at render time for states that reach the canvas through neither (imports, copilot edits), where unmounting the port would make React Flow drop the edge leaving it. The migration also moves any `data.errorEnabled` a developer created on this branch onto the column and strips the key; both statements match zero rows in production, where it never shipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(canvas): realign the Snowflake and Dynatrace sentences with staging's blocks Both breaks are the class the union check cannot see: separate hunks of the same file merged cleanly, and the result names fields that no longer exist. A sentence that does resolves to nothing, with no throw and no log. Snowflake's rewrite (#6474) moved database, schema, table, warehouse and procedure onto canonical selector pairs, so seven clauses anchored on ids that are gone. Each now names both members of its pair, which is also what keeps the card readable for someone working in advanced mode. Its nine new operations have sentences. Dynatrace (#6463) scoped the mute reason to the operations that mute, because unmuting accepts exactly one — so the two unmute sentences were asking for a field their card no longer shows. They drop the clause. Coverage is 4736/4736 operations across 321/321 blocks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(api): drop the block-data error flag from the workflow contract Left behind by the consolidation: the flag no longer lives in `data`, and a schema that still declares it there invites the mirror back through the wire. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(deploy): compare edge handles by port, so a falsy one cannot read as changed `loadWorkflowFromNormalizedTables` now runs handles through the canonicalizer, which falsy-coalesces — so an edge persisted with `sourceHandle: ''` loads as no handle at all. The server diffs that against the deployment version's raw jsonb, which still has `''`, and the set comparison reads one edge as removed and another added. Every workflow holding such an edge would ask to be redeployed the moment this ships, for nothing. Two write paths use `?? null` rather than `|| null`, so `''` is reachable. Canonicalized inside `normalizeEdge` rather than at either call site: the two sides are loaded by different paths and only some of them normalize, so the comparison has to be unable to tell two spellings of one port apart however its inputs arrived. This is the change reverted in 066e18a. That revert reasoned only about side-anchored ids, which are genuinely unreachable — it missed that the same coalesce collapses the empty string, which is not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(deploy): keep ignoring the error flag's old home in block data Deploying never converged: the badge asked to redeploy again the moment it finished, forever. The flag lived in `data` before it had a column. `0287` moves it, but a migration only reaches the live tables — every deployment version already written is frozen jsonb that keeps the old key. The wire schema no longer declares it either, so Zod strips it from the live state on its way to the client. So the two sides of the check genuinely differ: live `data: {}` against a snapshot's `data: {errorEnabled: false}`, reported as `data.errorEnabled` changed. Deploying cannot fix that — the next snapshot is taken from rows that still carry the key. Confirmed against a real stuck workflow: 18 versions, both blocks reporting `data.errorEnabled`, and the same two states comparing equal with this restored. Removing the exclusion in 7934df7 assumed the migration could reach every copy of the value. It cannot reach a frozen snapshot, so the comparison has to keep tolerating the old key regardless of where it survives. The block field is still compared on its own, with `!!`, so the flag itself is not ignored. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * revert(deploy): stop tolerating a block-data shape that never shipped The flag's stint inside `data` began and ended on this branch: `main` and `staging` have zero mentions of `errorEnabled`, and `ci.yml` gates every deploy job on a push to main/staging/dev, so opening a PR deploys nothing. No released version ever wrote the key, which means no production row and no production deployment snapshot can hold one. That makes the exclusion permanent code apologizing for a shape that cannot reach the database it defends. Migration 0287 already strips the key from the live tables, which is where a one-time data fix belongs; in production it matches zero rows, and on a developer database it makes the next deploy write a clean snapshot. Reverts 5ece9f9. That fix was correct about the mechanism and wrong about the scope: it read a local database as evidence about production. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * improvement(canvas): retract bystander cards during a run, slant the sweep mark Two things about a running canvas. A run pinned every card's action bar open and suspended every card's hover on top of it, so the canvas became a wall of open swells that could neither retract nor respond to a pointer — and the `isWorkflowRunning && !isRunning` hover treatment already written for those cards was unreachable. Only the card that is actually running is pinned now; the rest behave as they do at rest, which is what makes hovering one bring its bar up again. The sweep's filled slot painted a full 24px square. It now paints a slanted band across the slot, as a hard-stop gradient rather than a `clip-path` — the two end slots already carry one for the swell silhouette and a second would have to win a specificity race with it. The stops hold `--surface-2` exactly, so only the shape changes. Each variant is spelled out because Tailwind's JIT reads literal class strings and a composed `hover-hover:${FILL}` compiles to nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * improvement(canvas): fill the running sweep one way, and tighten its mark The sweep drained back to empty after each pass, which reads as undoing the progress the block is making. It fills left to right and starts over. The direction flag goes with it — the state is just the count now. The slanted mark also sat too far off its neighbours. Slant and tightness trade against each other here: the transparent wedge has to be at least as wide as the edge's horizontal travel, or the cut clips a corner instead of crossing the slot. Leaning 7° off vertical instead of 17° travels 2.9px across the 24px slot rather than 7.3px, which brings the wedge in from 26% to 12% — 3.2px a side against 7.8px, so the gap between marks drops from ~17.6px to ~8.4px with the slant still crossing cleanly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * improvement(canvas): fill the running bar once, edge to edge The sweep restarted from empty every time it filled, so the bar kept re-running ground it had already covered. It fills left to right once and holds. The mark also sat inset in its slot, which put a gap on both sides of every join and made the row read as separate chunks instead of one bar. It now spans its slot edge to edge, leaving only the row's own `gap-[2px]` between marks, and takes its weight off vertically instead: `bg-clip-content` with symmetric padding paints a 10px band inside the 24px slot without changing the slot's size, so the swell measured around it does not move. `--surface-2` is untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * improvement(canvas): paint the running bar as right-leaning uprights The fill read as a row of horizontal slabs. Each filled slot now paints one narrow upright bar leaning right, so a run fills as `/ / / /`. Geometry, since the two constraints fight: leaning the edge 15° off vertical carries it 4.3px across the bar's 16px height, so the transparent margin has to stay above 15% or the cut clips a corner instead of crossing top to bottom. 38%/62% leaves a 7px bar with room to spare. Height comes from `bg-clip-content` plus symmetric padding, which does not change the slot's own size, so the swell measured around it stays put. `--surface-2` is untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * improvement(canvas): pitch the running hatch to the row, not to one bar a slot One bar per 24px button left the rest of the button empty, so the marks inherited the button grid's rhythm and sat ~19px apart — a row of isolated ticks rather than a loader. The fill repeats now, at a pitch that divides the row's own rhythm: a slot plus its `gap-[2px]` is 26px, so a 13px horizontal pitch puts exactly two bars in every slot and stays in phase across the gaps, including the 40px end slots. Bars land every 13px with a uniform 6px between them, whatever the run's length. Stops are measured along the 105° axis rather than horizontally, so they carry the `sin(105°)` factor: a 7px bar on a 13px pitch is 6.76px on a 12.56px period. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(canvas): paint the running hatch once across the row, not per slot The bars came out bunched in some places and spread in others. Per-slot backgrounds cannot avoid that: each button starts its own gradient at its own origin, so the phase resets at every slot — and the end slots are 40px against the others' 24px, so the resets are not even uniform. Three passes of tuning the stops were all chasing a constraint the approach could not satisfy. The hatch is now one element spanning the row, so there is one gradient and one phase. It sits behind the buttons and grows by width: the run/stop button keeps an opaque fill while running and masks the part growing underneath it, and every other slot is transparent mid-sweep so the hatch reads through. The slots no longer paint anything themselves, and the per-slot filled flag goes with them. `--surface-2` is unchanged; only where it is painted moved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * improvement(canvas): close the running hatch's gaps The hatch ran a 50/50 duty cycle — 6px of fill to 6px of air — which read as sparse. It now runs 8px to 3px. Both stops are measured along the 105° axis rather than horizontally, so each carries a `sin(105°)` factor; the note records that, and that closing the gap further is a matter of moving the first stop toward the second. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(emcn): drop the brand highlight from popover menus (#6506) Context menus opted into a palette of their own — `variant='secondary'` for a brand-blue row highlight and `colorScheme='inverted'` for a dark card — so the canvas, block, toolbar, terminal, sidebar, and preview menus looked nothing like the menus everywhere else in the product. Removes both overrides so they inherit the same surface, border, and `--surface-active` highlight the terminal's overflow menu already uses, and drops the brand state from the Popover itself along with the `variant` prop that only ever selected it. One fewer way to style a menu. * feat(executor): opt-in per-block retry (#6505) * feat(executor): opt-in per-block retry Adds a per-block retry policy, off by default, surfaced in the editor's additional-fields disclosure alongside the block's other advanced settings. A block that opts in replays its handler while tries remain, then rethrows the final error so the error port behaves exactly as it does for a block that never retried — retrying only delays the existing outcome, never changes it. Retry is deliberately indiscriminate about the failure, since there is no reliable way to tell a transient error from a permanent one and classifying would silently do nothing for the generic errors people turn it on for. Only throws that are not failures are excluded: a deliberate stop, a child workflow whose own blocks already ran their policies, and the block types whose throw is control flow (human-in-the-loop, sentinels, subflow containers, notes, triggers). Eligibility lives in one predicate read by both the editor and the executor, so a block can never keep retrying after an edit that hides its control. `retry` is a nullable jsonb column; NULL means "runs once", which is how every existing block already behaves, so the change is inert until someone opts in. Bounds are clamped on read rather than rejected, so a value written before a bound moved still resolves to something runnable. Also decouples the additional-fields disclosure from `block.advancedMode`. That flag decides which member of a canonical pair serializes, so opening the disclosure used to be able to drop a block's configured credential. Expansion is now view state; the stored flag is no longer written by the editor. Retried blocks report their try count on the trace span, shown in log details. * fix(realtime): allow the write role to persist a block retry policy `update-retry` was added to the protocol but not to the write-role allowlist, so the editor applied the change optimistically while the server dropped it and the policy never reached the database. Adds a test asserting the write role holds every per-block operation the protocol declares, so the next block setting cannot repeat this silently. * fix(editor): keep a retry number field's value when it is blurred untouched Committing on blur normalized the draft unconditionally, and an untouched field's draft is null — which normalizes to the default. Focusing and leaving Max tries silently reset a configured 5 back to 3. * improvement(canvas): close the running fill to solid, slant its leading edge Gaps gone entirely: the bar is one solid fill now. The slant moves onto the growing edge, because a repeat with its gaps closed has no edges left to show. 4px of run across the 16px height is the same 15° lean the bars carried, so the fill still leans right — it just leans at its front instead of throughout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * improvement(canvas): return the running fill to the squares' rhythm, sheared Marks are 24px with the row's 2px gap after them again — the geometry the slots carried before any of this — so they land where the squares did. The shear is the only thing that is new. It stays on the single spanning element rather than going back to per-slot backgrounds: one gradient means one phase, which is what lets the 24/2 rhythm hold across the 40px end slots instead of resetting at every boundary. Stops are measured along the 105° axis rather than horizontally, so both carry a `sin(105°)` factor: 24px of mark is 23.18px of stop, and the 26px pitch is 25.11px of period. Writing 24/26 directly renders ~3.5% wide and drifts out of the squares' rhythm across the row. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(canvas): trim the running fill to the swell's tapered end The fill ran off the block. The row is a rectangle but the swell is not — its last slot cuts a diagonal so the shape narrows toward the top, and a rectangular overlay therefore painted past the gray edge up there while still sitting inside it at the bottom. The per-slot version never showed this because each button's own clip contained its fill; moving the paint onto one spanning element took that containment away with it. The overlay now carries the same taper, read off that slot's own path: 16.67px in from the row's right at the overlay's top, 3.33px at its bottom, a slope of 20/24. Only applied to the swell variant, which is the shape that tapers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(canvas): stop the handoff highlight pinning a bystander's toolbar open Dropping `isWorkflowRunning` from `forceOpen` was not enough: it also read `usesSelectedVisuals`, which is `isNodeSelected || isExecutionHighlighted`, and the handoff highlight covers the block feeding the running one. So the upstream card kept its bar down for the whole run — the wall of open swells this was supposed to end, one card smaller. Those are two different questions. `usesSelectedVisuals` still drives the TREATMENT — the graphite silhouette and `data-node-selected`, so the eye can follow the baton — while whether the toolbar is pinned open now keys off selection alone. The container keeps `isRunning` by itself. Selection was never a pin there, and its own tests hold it to opening on hover. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(realtime): refresh the error flag on a block upsert `BATCH_ADD_BLOCKS` wrote `errorEnabled` on insert but left it out of the conflict clause, so re-adding an existing block id kept whatever the row already held while every sibling flag — enabled, advancedMode, triggerMode, retry, locked — was refreshed from `excluded`. The client's value was silently discarded and the old error-output state came back on the next load. Mine: the insert side gained the field when the column landed and the conflict set did not. The other two block writers delete before inserting, so no stale row survives them; this upsert was the only path that merged into one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * update loader animation * change the loader --------- Co-authored-by: andresdjasso <andresdjasso@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Waleed <walif6@gmail.com>
* feat(salesforce): add JWT bearer flow and sandbox OAuth support Salesforce integration users could only authenticate through interactive OAuth, which an API-only integration user cannot complete — there is no UI for them to log in to. Adds the JWT Bearer Flow as a second grant on the existing service-account provider, and registers sandbox as its own authorization server so sandbox orgs can connect at all. The assertion is audienced at the org's My Domain URL rather than login/test.salesforce.com: Salesforce ended legacy hostname redirections in Spring '26 and External Client Apps now reject the generic sandbox host with app_not_found. My Domain is valid for Connected Apps and External Client Apps, production and sandbox alike, and is what the Salesforce CLI recommends — so the stored host alone determines the environment. Sandbox credentials are stored under their own provider id, mapped back to the one Salesforce service via additionalProviderIds on OAuthServiceConfig. That is threaded through every resolution point, including the two SQL filters that would otherwise have hidden sandbox credentials from the block picker entirely. Also fixes three latent bugs surfaced along the way: Zoom interpolated an undefined client secret into its Basic auth header, sandbox refresh tokens would have been posted to the production endpoint, and the sandbox connector would have been silently dropped as unconfigured. * fix(salesforce): canonicalize connected provider ids in the copilot credential tool A credential stored under an alternate authorization server was recorded in `connectedProviderIds` under its own id, while the not-connected list compares against the service's canonical id — so a sandbox-only Salesforce user was reported as both connected and not connected. Record the canonical id instead. Also types the JWT test's assertion decoder instead of returning `any`. * fix(salesforce): close reconnect, instance-URL, and key-handling gaps An independent audit swarm found four real defects in the JWT bearer work: - The credential update hook rebuilt its request body from a hand-written allowlist, so `authMethod`, `privateKey`, and `username` were silently dropped. A JWT private key could never be rotated through the UI, and switching grants failed with a generic error. Forwards the whole contract body instead, so a field added to the contract later cannot be lost again. - `getInstanceUrl` guarded only the `sub` claim against login-host origins, so a sandbox id token whose `profile` was rooted at test.salesforce.com yielded the login host as the org's API base. Both claims are now guarded, and a guarded-away `profile` falls through to `sub` instead of ending the lookup. - `canonicalizeServiceProviderId` replaces the previous fold, which also matched family-wide service-account ids and so dropped one arbitrary sibling product (Gmail, Confluence) from the copilot's not-connected list. - The private key was collected in a plain textarea, leaving browser spell check and autofill free to ship it to third parties. Also restores the explicit https check on the userinfo-derived instance URL, anchors the scope marker, caps the accepted RSA modulus, and stops single-grant providers paying for a stored-blob decrypt on every reconnect. Docs: the JWT path no longer tells readers to enable the Client Credentials Flow, and calls out the my.salesforce-setup.com host as the likely wrong paste. Adds coverage for the paths the audit proved untested: partitionClientCredentialFields, credentialProviderMatchesService's alternate-server clause, reconnect carry-forward, the typographic-apostrophe error branch, and the passphrase hint. * fix(salesforce): handle the Government Cloud JWT audience and unassigned-profile errors Verification against Salesforce's own sfdx-core surfaced two gaps: - `gs1` Government Cloud orgs have ordinary *.my.salesforce.com hosts, but Salesforce requires `https://gs1.salesforce.com` as the JWT audience. The host regex accepted them, so they would have failed with an opaque audience error. The token still posts to the org's own host; only `aud` differs. - `invalid_app_access` — Permitted Users is set to admin-pre-authorized but the run-as user's profile was never assigned to the app — is the likeliest misconfiguration and had no hint at all. Also sends `iat`, matching sfdx-core and every mainstream implementation, and softens two TSDoc claims that were stronger than the evidence: Salesforce does not hard-reject a far-future `exp` (its own CLI ships one), and My Domain is the right audience for commercial orgs rather than universally. * fix(salesforce): match sandbox credentials in Chat and the connect draft Two more surfaces resolved a credential to its service by exact provider id: - `credentialsForTarget` compared only `providerId`/`baseProviderId`, so a sandbox-only user's Salesforce chip in Chat read as disconnected and re-prompted them to connect. The alternate ids are passed in by the caller rather than resolved in the module, which is `'use client'` and would otherwise pull the OAuth provider registry into the chat bundle. - `createConnectDraft` resolved the service name by exact id, so a sandbox connect defaulted to the label "My salesforce-sandbox". * fix(salesforce): carry alternate provider ids through chat connect verification The chip's live target was widened to match a sandbox credential, but the post-connect verification leg re-reads the STORED attempt, which did not carry the ids — so completing a sandbox connect from Chat was detected as a failure and the chip was marked failed. The attempt now persists them; attempts written before this simply match as they did, and they expire within 15 minutes. Also marks the auth-method picker required while it is the field blocking submit on a reconnect, so the greyed button has a visible cause. * fix(salesforce): send reauthorize to the server that issued the credential "Update access" derived its provider from the service id, which always yields the primary authorization server. A sandbox credential missing a scope sent the user to login.salesforce.com — where a sandbox-only user cannot sign in at all, and where a user who can sign in creates an orphan production account while the banner never clears. Both credential selectors now pass the selected credential's own provider id, which the connect modal already honours. Also names the alternate provider ids explicitly in the disconnect sweep. That branch is unreachable today (every caller sends an accountId), but it was catching them only by the `{base}-` prefix accident. * fix(salesforce): make the Government Cloud audience check exact, not a prefix `startsWith('gs1-')` was invented from a paraphrase of sfdx-core and would have misrouted an ordinary org like gs1-widgets.my.salesforce.com to the GovCloud audience — breaking a setup that works today. sfdx-core's host signal is the literal gs1.my.salesforce.com; its other signal is the org's createdOrgInstance, which we never see. Matching exactly means a miss falls back to My Domain, which is the behaviour before the branch existed, while a false positive cannot happen. Also replaces the hand-rolled origin regex in getInstanceUrl with URL parsing, which normalizes userinfo, ports, and case before the login-host comparison, and drops two error hints that had no evidence behind them.
The new workflow block card (#6458) imports 16 icons from `lucide-react`, which #6241 removed from the dependency tree in favour of the in-house `@sim/emcn/icons` set. `next build` fails on both files: Module not found: Can't resolve 'lucide-react' It builds green locally because `fumadocs-ui` pulls lucide-react in for `apps/docs` and the install hoists it into `apps/sim/node_modules`, so dev, `tsc`, and biome all resolve it. Only an install that excludes the docs app — the Docker build — sees it missing. `check-import-specifiers` does not cover this, as it skips bare npm specifiers by design. Eleven of the icons already existed in the house set. Three map onto existing glyphs that are already the same drawing: `Braces` -> `TypeJson` (curly braces), `Hash` -> `TypeNumber` (hash), `KeyRound` -> `Key`. The remaining four are new, ported with the same transform #6241 used — lucide geometry scaled 0.86 and translated so its (12, 12) centre lands on (10.25, 9.75) in a `-1 -2 24 24` viewBox, stroked at the house 1.55. Every icon ported in #6241 sits on that centre; these four measure there exactly. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…6526) * docs(salesforce): correct the setup steps that would strand an admin Verified the guide against Salesforce's current UI and docs. Most of it holds; these do not: - The Client Credentials step told admins to check "Enable Client Credentials Flow" under OAuth Policies. On an External Client App that checkbox is under Edit Settings → OAuth Settings; the Policies page holds only the Run As picker, so anyone following it literally hunts for a control that is not on the screen. The FAQ answer inherited the same conflation. - Pre-authorizing the app must go through the profile or a SECOND permission set. A permission set backed by the Salesforce API Integration license cannot hold an Assigned Connected Apps section at all, so the app can never be assigned from the same permission set that grants object access — which produces exactly the "user hasn't approved this consumer" failure that step exists to prevent. This is the likeliest way a JWT setup fails. - Salesforce requires an RSA key of at least 2048 bits; an ECDSA key is silently rejected, and the certificate must stay under 4 KB. - The JWT toggle does not appear until Enable OAuth is on, and the control is "Upload Files". Also scopes the capability promise for the API-only license: SOQL and CRUD on standard objects are supported, reports and dashboards are genuinely unverified in either direction, and Apex Class Access is a permission this license cannot hold, so Tooling API calls touching ApexClass will fail. * docs(salesforce): align the Developer Edition host in the FAQ with the setup section The setup section was corrected to make the `-dev-ed` suffix conditional, but the FAQ still presented it as mandatory — so an admin whose Developer Edition domain lacks the generated suffix would read two contradictory formats on the same page and validate against a host that does not exist.
…ate (#6528) * fix(logs): keep run provenance when compaction drops the execution state Oversized-payload compaction drops executionState wholesale but keeps secretProjectionVersion, so the display projection saw a contract-marked row it could not verify and returned structural-only spans — blanking every input and output in the trace. Store the provenance top-level so it survives compaction, omit it from both display projections (it carries encrypted secret values and their names), and let rows truncated before this shipped keep the spans they were already projected with at write time. * improvement(logs): type the new test helpers instead of using any
…#6527) * fix(chat): stop the streaming transcript floor inventing scroll space The sizer floor was the viewport's bottom edge (scrollTop + clientHeight), which exceeds the content height whenever the transcript is shorter than the viewport. That invents scrollable space no content occupies, and a mid-turn container shrink turns it into real scroll room the bottom-pin scrolls into. Clamp the floor to the space content has actually held this turn: the max of the virtualizer's total size and the still-applied floor. The applied-floor term keeps undrained debt across a turn boundary that interrupts the drain. * fix(chat): release the transcript floor when the chat changes The high-water mark and applied floor are per-turn refs on a component that survives a chat switch, so a tall chat's mark could size a newly opened short one for as long as the outgoing turn kept the floor engaged. Release both outright on a chat change — the switch re-lands the viewport, so there is no eased settle to preserve — while treating a pending chat adopting its id as the same conversation. Also switch the sizer-floor import to the absolute path convention.
* feat(incidentio): add on-call, alert, catalog, and team tools
Adds 19 tools to the incident.io block, taking it from 46 to 65
operations. Every endpoint, param, and response field is taken from the
official OpenAPI spec at api.incident.io/v1/openapiV3.json.
Who is on call had no reachable answer before: the data lives in
ScheduleV2.current_shifts, and both schedules_list and schedules_show
returned it but never declared it. The new incidentio_on_call_now tool
flattens current and upcoming shifts to one row per person, and the two
existing schedule tools now declare the fields they were already
returning.
Also fixes two pre-existing wiring bugs: the block declared an output
named schedule_override while the tool emits override, and the on-call
handoff skill described a lookup the integration could not perform.
* fix(incidentio): stop the alert filter sentinel reaching the API
The has_notes and include_maintenance_window dropdowns default to the
string "any", meaning "do not filter". The params transform skipped the
key in that case, but the executor merges its output over the raw inputs
(`{ ...inputs, ...transformedParams }`), so the sentinel survived and the
tool sent has_notes[is]=any, which incident.io rejects.
The transform now always assigns the key, mapping "any" to undefined so
it overwrites the sentinel instead of leaving it in place. The tool also
only serializes these filters when it actually has a boolean.
Adds tests covering the sentinel, both real boolean values, and the
documented bracket-operator filter syntax.
* fix(chat): stop losing sends aborted during mount-settling
* fix(chat): detect aborts by signal state, not error identity
fetch rejects with the RAW abort reason when its signal carries one —
abort('unmount:client_cleanup') surfaces as a plain string, so every
err.name === 'AbortError' check missed it and the restore path never ran
(verified live). The test stub now rejects with the raw reason like real
fetch, which turns this gap red.
* fix(chat): hand an aborted chatless send to the next mount
The mount-settling cycle is a full remount — the pending chat key is
regenerated per instance, so restoring the aborted send into the dead
instance's queue orphaned it (verified live). A chatless send now
re-persists as a one-shot MothershipHandoffStorage handoff the next
mount's consumer re-sends; chat-bound sends keep the queue restore.
* fix(chat): deliver an aborted chatless send to the live replacement surface
The settling remount's consumer checks handoff storage before the
restore microtask re-persists it, so the stored handoff sat unread until
a navigation. The replacement surface's send listener IS registered by
restore time — deliver the message directly through the claimable send
event, keeping the stored handoff as the no-surface fallback.
* refactor(chat): thread the recoverable-abort outcome through the send result
Replaces the restorableCleanupAbortRef reset choreography with a widened
startSendMessage return ('recoverable_cleanup_abort'), so the restore
decision is ordinary data flow and the second caller cannot leave a stale
flag behind.
* fix(chat): carry attachments through the cross-mount send handoff
The recoverable-abort delivery excluded attachment-bearing sends, so
they restored under the dead instance's pending key and were silently
lost. The claimable send event now carries fileAttachments end to end
(dispatcher, home listener, restore path); only the storage fallback —
whose shape cannot hold attachments — still queue-restores them.
* fix(panel): forward event attachments to the copilot send
* fix(chat): carry attachments through the stored handoff lane too
The unclaimed-event fallback excluded attachment sends and restored
them under the disposed mount's pending key. The persisted handoff now
carries fileAttachments (they are plain references to already-uploaded
files), the home consumer forwards them, and the recovery branch always
hands off — no stranded lane remains.
* fix(chat): probe the orphaned stream before re-sending a withdrawn send
The cleanup-abort recovery treated "no response headers yet" as "the server
never got it" and re-sent. It is not the same thing: the mothership chat route
never reads `request.signal`, so a request it had already accepted still runs
to completion — resolveOrCreateChat, persistUserMessage, and the billed turn
all commit even though the client socket is gone. Re-sending blind therefore
left the user with two chats and two billed runs for one message.
Recovery now carries the withdrawn send's `userMessageId` as a stream id
through both lanes (the live `mothership-send-message` event and the stored
one-shot handoff) and through a restored queue entry. Before re-sending, the
dispatcher polls that stream: when it resolves to a chat, the server already
has the message, so the chat is adopted instead of sent again. Only a stream
the server has no record of — a 404, i.e. genuinely never accepted — re-sends.
Timing out re-sends too, which is the safe direction.
Also corrects the root cause recorded in the comments. A Suspense hide/reveal
cannot run this cleanup: React 19 disappears layout effects only, and this is
a passive effect (verified against react-dom 19.2.4). What does run it is
StrictMode's dev double-mount and a real client-side navigation away, both
mid-flight — and because MothershipHandoffStorage consumes atomically, the
replacement mount finds nothing left to retry.
* fix(chat): never re-send on an unresolved probe, and reconnect after adopting
Two defects in the orphaned-stream probe, both found by Bugbot.
A probe cut short by an epoch change (unmount, chat switch) returned the same
`undefined` as "the server has no such stream", so the dispatcher fell through
to `startSendMessage`. After unmount the teardown has already dropped the abort
controller, so that send opened a POST nothing could cancel — duplicating the
very message this recovery exists to protect. The probe now reports
`superseded` distinctly and the dispatcher leaves the entry queued, keeping its
`recoverStreamId` so a later mount probes again.
Adopting the recovered chat also invalidated only the chat list. Hydration
reconnects to a live turn solely on `chatHistory.activeStreamId`, and that
query is cached for MOTHERSHIP_CHAT_HISTORY_STALE_TIME — on a chat-bound
recover the client normally holds a copy predating this stream, so the adopted
chat rendered with the running response invisible. Adoption now invalidates the
chat detail too.
Both regression tests were confirmed to fail without their fix: the first
re-sends (2 POSTs instead of 1), the second never invalidates. The probe stub
gained a `pending` mode because a `gone` probe answers on the first attempt and
leaves nothing in flight to interrupt — the earlier draft of the first test
passed with the guard removed and proved nothing.
* test(chat): cover the departing surface's own recovery-event claim
Greptile flagged that a surface being torn down could claim the recovery event
its own cleanup emits — which would return `true`, suppress the storage
fallback, and strand the message under a disposed pending key. It cannot: React
removes the listener during the same synchronous unmount commit, while the
recovery runs from the fetch rejection a microtask later, so by then nothing of
the departing surface is listening.
That ordering was previously only argued, never asserted — the suite unmounted a
bare hook with no listener attached. This mounts a home.tsx-shaped surface that
both drives useChat and registers the claiming listener, and asserts the
departing listener claims zero times while the handoff still reaches storage.
Confirmed meaningful: neutering the listener's removeEventListener cleanup so it
survives teardown makes it claim, and the test fails.
* fix(chat): hand off a chatless send when the probe is superseded
The previous commit made a superseded probe leave the entry queued rather than
re-send it. That is the right retry for a chat-bound key, which is the stable
chat id, but wrong for a chatless one: a `pending::` key is regenerated every
mount, so anything left under it is unreachable and the message is stranded —
the same loss this PR exists to prevent, just reached by a different route.
A superseded probe on a pending key now goes through the same recovery lanes as
the cleanup-abort path (live replacement surface, else a one-shot stored
handoff), still carrying the stream id so the next surface probes before it
sends. Skipped when the entry is no longer under that key, since adoption
migrating it to a live chat already leaves it recoverable there. The lane is
extracted so both call sites share one implementation.
The existing superseded test only asserted that nothing sent, which this bug
satisfied trivially; it now also asserts the message survives. Confirmed red
without the fix.
---------
Co-authored-by: Waleed Latif <walif6@gmail.com>
…row hover states (#6534) * improvement(ui): align terminal with the workflow design system, fix row hover states Terminal: - derive log-row block tiles the way the canvas does (role accent for core blocks and subflows, provider colour only for role-less integrations) - compose rows from chipGeometryClass, chipContentLabelClass and disclosureChevronClass instead of re-deriving the pill - align the output tree's greys with the log rows; share ROW_STYLES.nested and BADGE_STYLE instead of duplicating the literals - neutralise value-type badges so red is the only colour in the tree - unify the row/separator gutter; normalise icon sizes to size-[14px] - drop dead flattenEntryTree and the RunningBadge re-exports Hover model (chipVariants, PopoverItem, Combobox, docs sidebar, terminal): - hover paints --surface-hover, one step below the --surface-active a selected row keeps, so a hovered row no longer impersonates the selected one - an active row holds its surface through hover instead of brightening Deploy modal: - move the footer actions onto the Chip family, primary action as Chip variant='primary' to match every other modal footer * improvement(emcn): extract the row-state surface pair, simplify the terminal rows Review follow-ups from /simplify and /cleanup: - add chipHoverSurfaceClass / chipActiveSurfaceClass to chip-chrome as the one home for the two-surface row model, and route chipVariants, PopoverItem, Combobox, the docs sidebar, the landing preview and queued messages through them instead of restating the literals - terminal ROW_STYLES now renders chipVariants rather than re-deriving its output, and the four rows share content/label/status classes resolved once - structured-output composes chipGeometryClass with an h-auto override rather than restating four of its literals - getEntryAccentType collapses to one expression, dropping the SYNTHETIC_BLOCK types coupling - collapse the chip compound variants to two array-matched entries - deploy modal: hoist the shared loader adornment, size it with chipContentIconClass so it matches every other chip icon - landing preview drops its --c-active/--c-hover inline aliases for the tokens - trim the rationale to one canonical copy with cross-references, and convert the block comments on declarations to TSDoc * improvement(workflow): align canvas controls with the canvas surface and icon scale - floating controls sit on --surface-2, the surface the block cards use, rather than --surface-1 (the sidebar/panel surface) - undo/redo/fit glyphs drop 16px -> the platform's 14px default, and the mode dropdown's own 12px icons come up to match; the control had three icon sizes - inactive buttons hover to --surface-hover instead of --surface-5, which was the active mode button's resting fill, so hovering one looked selected - inner radius goes concentric with the 4px padding (rounded-sm inside rounded-lg) * improvement(workflow): give the canvas-mode chevron the same treatment as its siblings It was the only control in the cluster with no hover fill and a different rest colour (--text-muted against the others' --text-secondary), so it read fainter and behaved differently under the pointer. It is also a disclosure chevron, so it now uses disclosureChevronClass instead of a hand-rolled duration-100 copy, and a real 20px box instead of the !p-1.5 override plus -m-1 hit-area hack. * test(terminal): lock the log-row accent rule against the block toolbar getEntryAccentType encodes a cross-surface rule — a block must be accented the same way in the terminal as in the block toolbar — and nothing enforced it. The table covers every branch: core blocks mapped and unmapped, role-bearing and role-less integrations and triggers, the config-less subflows, and the synthesized error/validation/cancelled rows that must keep their status fill. Verified failing: reverting the guard to an unconditional return reds two of the four cases. * fix(workflow): make notifications track panel and terminal resize live The toast stack insets by --panel-width / --terminal-height, but a resize drag writes those to the resized subtree only (.panel-container / .terminal-container) rather than to :root, because a custom-property write on :root recalculates the whole document (~150x slower). The stack is portalled to <body>, so it shares no ancestor with either and kept reading the stale :root value — it held its pre-drag position and jumped once the drag committed, while the canvas controls, which are laid out inside the shrinking canvas, tracked the drag in realtime. useDragResize now accepts several target subtrees and writes each one, so the scoped recalc is preserved and every consumer follows the drag frame by frame. The stack is found through a new data-toast-viewport attribute. Also drops the canvas controls from bottom-4 to bottom-2: the toast clears the terminal by 8px (it anchors from the viewport, and the terminal is inset by CONTENT_WINDOW_GAP), where the controls measure from the canvas floor and so sat at twice the gap. * improvement(workflow): inset the canvas controls 8px off both edges The toast stack clears the terminal and the panel by 8px — it anchors from the viewport at --terminal-height/--panel-width + 16px, and both are themselves inset by CONTENT_WINDOW_GAP (8px). The controls measure from the canvas floor and wall instead, so their 16px read as twice the gap on both axes. * improvement(workflow): lift the canvas controls and toasts to a 12px clearance 8px sat them too close to the terminal. 12 is on the same 4px grid as the surrounding spacing, where 10 would have been the only off-grid value in the area. Both surfaces clear the terminal and the panel by the same amount, so they read as one row; the toast's literals move into named insets rather than staying bare numbers in a style object. * refactor(hooks): split the drag's resize target from its other var consumers getTarget briefly accepted a list, which made the first entry both the resized element and the drag's liveness reference. A toast auto-dismisses after 5s, so had one ever led that list, its mid-drag unmount would have read as the drag target detaching and skipped the final recompute on release. The co-consumers now come through getExtraTargets, which is written but never consulted for liveness, and can come and go freely. * fix(emcn): stop the combobox cursor diverging from what Enter commits The option rows painted --surface-active from CSS :hover as well as from isHighlighted. CSS :hover tracks the pointer continuously while highlightedIndex only advances on mouseenter, so once the list scrolled under a stationary pointer the row that looked selected was not the one Enter would commit — Enter reads filteredOptions[highlightedIndex]. isHighlighted is now the single source of truth for the cursor, so paint and commit cannot disagree. The row under a stationary pointer may lag a scroll until the mouse moves, but it lags in agreement with what Enter will do, which is the invariant worth keeping. Disabled options also stop painting on hover, matching the mouseenter guard that already refused to highlight them. The 'All' row keeps its own hover: it clears the highlight rather than taking it, so it has no isHighlighted paint to fall back on. * fix(toast): derive the workflow inset from the shell's actual padding WORKFLOW_INSET_PX baked in the 8px the workspace shell normally insets the panel and terminal by, so the stack's 20px resolved to a 12px clearance — matching the canvas controls. But the shell drops to p-0 on the desktop title-bar shell with a collapsed sidebar, and there the stack would have sat 20px out while the controls, laid out inside the shell, stayed at 12. The stack now adds --workspace-content-gap (published on :root, zeroed by the same condition that zeroes the padding) to a flat 12, so the two surfaces hold the same clearance in both configurations. Before this PR they matched in the p-0 case at 16px each, so this closes a divergence the PR would otherwise have introduced.
* fix(chat): stop losing sends aborted during mount-settling
* fix(chat): detect aborts by signal state, not error identity
fetch rejects with the RAW abort reason when its signal carries one —
abort('unmount:client_cleanup') surfaces as a plain string, so every
err.name === 'AbortError' check missed it and the restore path never ran
(verified live). The test stub now rejects with the raw reason like real
fetch, which turns this gap red.
* fix(chat): hand an aborted chatless send to the next mount
The mount-settling cycle is a full remount — the pending chat key is
regenerated per instance, so restoring the aborted send into the dead
instance's queue orphaned it (verified live). A chatless send now
re-persists as a one-shot MothershipHandoffStorage handoff the next
mount's consumer re-sends; chat-bound sends keep the queue restore.
* fix(chat): deliver an aborted chatless send to the live replacement surface
The settling remount's consumer checks handoff storage before the
restore microtask re-persists it, so the stored handoff sat unread until
a navigation. The replacement surface's send listener IS registered by
restore time — deliver the message directly through the claimable send
event, keeping the stored handoff as the no-surface fallback.
* refactor(chat): thread the recoverable-abort outcome through the send result
Replaces the restorableCleanupAbortRef reset choreography with a widened
startSendMessage return ('recoverable_cleanup_abort'), so the restore
decision is ordinary data flow and the second caller cannot leave a stale
flag behind.
* fix(chat): carry attachments through the cross-mount send handoff
The recoverable-abort delivery excluded attachment-bearing sends, so
they restored under the dead instance's pending key and were silently
lost. The claimable send event now carries fileAttachments end to end
(dispatcher, home listener, restore path); only the storage fallback —
whose shape cannot hold attachments — still queue-restores them.
* fix(panel): forward event attachments to the copilot send
* fix(chat): carry attachments through the stored handoff lane too
The unclaimed-event fallback excluded attachment sends and restored
them under the disposed mount's pending key. The persisted handoff now
carries fileAttachments (they are plain references to already-uploaded
files), the home consumer forwards them, and the recovery branch always
hands off — no stranded lane remains.
* feat(search): improve command palette results
* feat(search): sharpen command palette discovery
Unify command surfaces, flatten ranked results, and remove favorites so the palette stays focused on fast discovery. Add Tab result cycling and workspace identity icons for quicker keyboard navigation.
* feat(search): unify cmd+k into one page-aware section model
Every palette view now derives from a single rule: the page's action group,
its own entity section hoisted, Platform actions, then a fixed tail shared by
all pages. Adds page commands for table/file/KB details, logs, and deploy; a
Logs section with run dates; chat last-activity receipts; kebab-cased
secondary search text with per-entry scattered matching; exact-section-name
ranking lifts; and scroll/selection fixes on open, loop, and arrow
navigation. Removes the canvas block/tool/trigger/docs sections and the
store's unused section restriction and pending-connect plumbing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(search): ask-Sim tab mode, canvas sections, and palette refinements
Tab now flips the palette into ask mode — Enter lands on Chat with the query
seeded via the proven curated-prompt handoff (auto-send deferred on a
diagnosed use-chat mount-abort bug) — replacing the no-results New Chat
fallback. Restores the canvas Blocks/Triggers/Tools/Tool operations sections
between the workflow Actions group and Sim, keeps the integrations catalog
and connected accounts off the canvas, renames the global group to Sim and
page groups to Actions, puts an exactly-named page above its lifted contents,
adds chat last-activity receipts, and softens the list chrome (hidden
scrollbar, shorter fade, scroll-margin fixes for arrow and loop navigation).
Also renames the generic webhook block to Webhook.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(search): order module results below Sim actions
* fix(review): make command palette fast and focused
* fix(search): restore Ask Sim prefill and refine palette ranking
* chore(sidebar): drop unused getSettingsHref destructure
* fix(deploy): gate every deploy invoker on full eligibility
* fix(search): port the palette to post-#6458 staging
Carries the former merge resolutions as one commit: the emcn icon set
(SelectAll fit-to-view, Search chrome), native browser-panel occlusion
gating, scheduled-tasks retirement, the chip-aware handoff consumer
superseding the ?handoff=1 machinery, and Knowledge bases pluralization.
* feat(search): auto-send Ask Sim queries via the chat handoff
* revert(search): return Ask Sim to prefill, storing raw prose
Auto-send still loses the message on cross-route navigation — use-chat's
cleanup abort fires during Home's mount-settling effect cycle. Prefill
restored, but via LandingPromptStorage directly so free-form queries are
never mentionified into @ chips.
* feat(search): auto-send Ask Sim queries via the chat handoff
Re-lands the auto-send flip: with fix/mship-mount-send-loss beneath this
branch, sends started during Home's mount-settling window survive the
cleanup abort (queued, restored, re-dispatched), so the handoff no longer
loses the query on cross-route navigation.
* fix(deploy): include registry loading in the deploy invoker gate
* improvement(search): label the ask row New Chat
* refactor(search): apply simplify-pass cleanups
- rank against a deferred query and hoist search-independent derivations
so typing never blocks on the cross-section re-rank
- cache secondary-text tokenization (hottest per-keystroke loop)
- skip building browse groups mid-search; gate the palette-only
credentials and logs queries on the palette being open
- flatten getGlobalSearchResults onto a spec-stable sort
- drop the dead store-side SEARCH_SECTIONS/docs path, the no-op
CommandSearch surface variant, a duplicate hex regex, a dead
font-base class, and the TaskItem/FolderedItem shape overlap
* fix(search): paint the palette fog with the dialog's own surface
The frost under the floating input reused the canvas card's --surface-2
gradient, which reads as a tinted band on the palette's --surface-4/
--surface-5 dialog (visible in dark mode). The CommandSearch surface
variant returns — this time with genuinely different values — and the
chrome test pins the host-matching fog.
* fix(search): the palette fog matches the inner --bg panel, not the dialog ring
* fix(search): review-round parity and consistency fixes
- table import command respects the in-progress upload gate
- Export CSV is offered to viewers (matching the header control)
- palette mode flips with the deferred query the ranking ran against
- a gated palette deploy reports the button tooltip's reason via toast
* fix(deploy): use the emcn toast input shape
* fix(search): rename logs view toggles to "Switch to Logs/Dashboard"
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…updated docs based on openapi spec (#5273) * v0.6.29: login improvements, posthog telemetry (#4026) * feat(posthog): Add tracking on mothership abort (#4023) Co-authored-by: Theodore Li <theo@sim.ai> * fix(login): fix captcha headers for manual login (#4025) * fix(signup): fix turnstile key loading * fix(login): fix captcha header passing * Catch user already exists, remove login form captcha * improvement(external-endpoints): v2 versions with clean signatures + updated docs * feat(usage): accept X-API-Key on usage-logs list + export /api/users/me/usage-logs and /export now use checkHybridAuth — the same auth /api/users/me/usage-limits already accepts — so external monitors can read summary.bySourceCredits (the source breakdown of usage-limits' aggregate currentPeriodCost) instead of estimating Copilot spend by subtraction. Workspace-scoped keys are pinned to their own workspace's slice of the ledger: the filter defaults to the key's workspace and an explicit mismatch 403s. Both endpoints documented in openapi-core.json. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz * feat(billing): dedicated v2 usage endpoints; keep internal usage routes session-only Replaces the earlier X-API-Key enablement on /api/users/me/usage-logs with a dedicated public surface, so the internal Billing-settings endpoints can evolve with the UI while external monitors get a stable versioned contract: - GET /api/v2/billing/usage — current-billing-period summary with bySourceCredits (the source breakdown external monitors need to watch e.g. Copilot consumption without estimating by subtraction), plus limitCredits and plan - GET /api/v2/billing/usage/logs — cursor-paged credit ledger in the v2 envelope - workspace-scoped keys are pinned to their own workspace's slice; personal keys read the account ledger The public wire is credits-only: usage-logs rows now carry a hasCost boolean instead of dollarCost (the Billing UI only needed the >0 signal), and the rateLimit block is removed from the usage-limits response and docs (deploy-modal tab relabeled accordingly). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz * feat(docs): validate OpenAPI specs against the Zod contracts in CI The specs in apps/docs are hand-authored because they carry what Zod never defines — error envelopes, status codes, prose, examples — so they can't be generated; check:openapi validates them instead: - spec integrity: $refs resolve, operationIds unique, 2xx documented, no orphaned component schemas - v2 conventions: every /api/v2 operation documents 401 + 429 and every 4xx/5xx resolves to the canonical { error: { code, message } } envelope - contract cross-check: contracts are auto-discovered from lib/api/contracts/v2 (each carries its method + path); doc<->contract coverage both ways, query/body/response field diffs via z.toJSONSchema - examples: documented request/response examples must parse with the matching contract's actual Zod schemas First run caught real drift, fixed here: 16 stale orphaned schemas in the core spec, the v2 billing ops referencing v1-shaped error components, deploy/rollback examples missing the required nullable lifecycle keys, CreateTableBody missing folderId, a legacy-grammar delete-rows example, and four knowledge document ops missing their required workspaceId query param. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz * fix(docs): recursive field diff in check:openapi + the deep drift it found A mutation test showed the doc<->contract field diff only compared top-level properties, so a typo inside the { data } envelope passed. The diff now descends through matching object properties and array items (both sides must expose a property set — passthrough contracts and prose-only docs end the descent instead of false-positive), with the Zod JSON-schema root doubling as the $defs context. Deep drift it immediately caught, fixed here: select-column config (options/multiple) missing from every tables column schema, AddColumnBody hand-rolling a third column shape (now composed from ColumnInput, with position/workflowGroupId as the per-op extensions the contracts actually admit), chunking strategyOptions undocumented, and the deployment lifecycle fields (activeDeployment/latestDeploymentAttempt) missing from DeploymentState. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz * fix(security): close the triggerType rate-limit bypass on workflow execute Caller-supplied triggerType flowed unchecked into preprocessExecution, whose checkRateLimit default turns OFF for 'manual'/'chat' — so any API-key caller, and any anonymous public-API caller billed to the workspace owner, could execute unthrottled by sending {"triggerType":"manual"} (async runs also skipped the worker-side check via admissionCompleted). External callers may now only send the redundant 'api' value; internal JWT callers ('workflow'/'mcp') are unaffected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz * refactor(execution): extract enqueue/status/cancel into shared libs Prepares the v2 execution surface: handleAsyncExecution's queue logic moves to lib/workflows/executor/enqueue-execution.ts (slot/claim semantics encoded in a discriminated outcome, not HTTP statuses), the execution-status read to execution-status.ts, and the order-sensitive cancel machinery to lib/execution/cancel-workflow-execution.ts. The v1 routes re-render identically — their suites pass unmodified. Also: preprocessExecution gains rateLimitCounter ('sync'|'async') and its 429 now carries code RATE_LIMIT_EXCEEDED + retryAfterMs (previously indistinguishable from the concurrency 429 and Retry-After was discarded); and the duplicate cancel contract in contracts/logs.ts is unified on the full 5-value reason enum — its narrower copy made requestJson throw a client ZodError when cancelling a paused HITL run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz * feat(execution): callable execution service + structured error classifier executeWorkflowService composes the same libs the v1 route holds inline (call-chain guard, execution-id claim, LoggingSession, preprocessing, deployed-state load + file-field processing, timeout-bound executeWorkflowCore, output hydration/compaction) for the deployed-state caller class — the seam the v2 execute route and in-process internal callers share, making the HTTP endpoint syntactic sugar. classifyExecutionError stops discarding the block context that buildBlockExecutionError already attaches at throw sites: failed runs now yield {message, code, blockId, blockName, blockType} with a stable append-only code enum (TIMEOUT/CANCELLED/USAGE_LIMIT_EXCEEDED/ INVALID_INPUT/BLOCK_EXECUTION_FAILED/CHILD_WORKFLOW_FAILED/ OUTPUT_TOO_LARGE/EXECUTION_FAILED), so callers route on error class instead of substring-matching messages — the single place raw errors are interpreted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz * feat(api): POST /api/v2/workflows/[id]/execute Thin route over executeWorkflowService: X-API-Key or anonymous public-API auth (sync/stream only for anonymous), strict body with body-flag async (no mode headers on v2), SSE passthrough for stream, and the execution resource response — executionId always present, in-band run failures are status:'failed' with the structured {message, code, blockId, blockName, blockType} error, sync timeout is status:'failed' + TIMEOUT instead of v1's 408, and a Response block's payload stays inside output (authors never control response status/headers on this origin). Async debits the async bucket and the 202 statusUrl points at the v2 executions resource. Adds CLIENT_CLOSED_REQUEST/SERVICE_UNAVAILABLE to the v2 error codes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz * feat(api): v2 executions status + cancel with queued backfill GET /api/v2/workflows/[id]/executions/[executionId] is the single status URL for sync and async runs: before the async worker writes the durable log row, status is backfilled from the job queue (deterministic job id) as 'queued'/'running' — closing v1's 202-to-pickup 404 window — and failed runs carry the structured error object. POST .../cancel renders the shared cancellation lib in the v2 envelope with the tightened 5-value reason enum. Both authenticate via the shared resolveV2WorkflowAccess (X-API-Key, authz masked as 404, allowPersonalApiKeys honored). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz * feat(execution): workflow tool + MCP bridge run in-process workflow_executor (workflow-as-agent-tool) short-circuits in executeTool through WorkflowBlockHandler — the same invocation boundary canvas child workflows use — mirroring the deployed_block_executor precedent. The MCP serve bridge calls executeWorkflowService directly instead of fetching its own execute endpoint; deployment-version pinning, MCP response-size rejection, and the actor override become typed options instead of header sniffing. Both callers drop the double admission slot and duplicate top-level log row the HTTP hop cost, and failed child runs now surface the structured error + child executionId so parents and MCP clients can route on error class and hand providers a reproducible handle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz * feat(infra): CORS + CSP coverage for the v2 execute path /api/v2/workflows/:id/execute gets the same wildcard-origin, credential-free CORS policy as v1 (the default credentialed policy would block browser API-key calls and open a cookie CSRF surface) with X-Sim-Stream-Protocol allowed and no X-Execution-Mode (async is body-selected on v2), plus the COEP/COOP/CSP header block. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz * feat(ui): deploy modal + copilot advertise the v2 execute surface All 20 API-tab snippets move to POST /api/v2/workflows/{id}/execute with the nested {"input": ...} body, async as the "async": true body flag (X-Execution-Mode gone), status polling against the v2 executions resource, the third tab renamed Usage and pointed at /api/v2/billing/usage, and {data} envelope unwraps in the printed responses. Fixes the latent baseUrl derivation (endpoint.split('/api/workflows/')) that would have silently built garbage URLs under a v2 endpoint, and deletes dead code (exampleCommand across 3 sites, getAsyncExampleTitle). Copilot deploy/manage/serializer endpoint builders and the api_trigger bestPractices example follow (the latter also drops its hardcoded staging host). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz * docs(api): document the v2 execution surface Adds execute, execution status, and cancel to openapi-v2-workflows.json with the structured ExecutionError schema (append-only code enum + block attribution) and the ExecutionResource contract, documenting the rules that differ from v1: modes are body-selected, a failed run is HTTP 200 with status 'failed', an executionId always means data (never the error envelope), queued status is visible immediately, and Response-block payloads stay inside output. Registers the three pages in the generated workflows meta.json and bumps the route-count baseline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz * feat(api): gate the whole /api/v2 surface behind one flag; UI stays on v1 Every v2 route now runs exactly one check immediately after auth — v2ApiGateError — and answers 404 when the `v2-api` flag is off, so the surface is invisible until it is deliberately rolled out. The gate is keyed on userId only: a workspace/org-keyed check would have to read membership for a caller-supplied id before authorization runs, and its 404-vs-403 split would leak cohort membership (the trap the per-domain table gate worked around by running late). The two executions routes inherit it from the shared access resolver; the tables-specific gate is removed so no route checks twice. `tables-v2-api` stays, now gating only the internal predicate-grammar route /api/table/[tableId]/query — note v2 tables routes move to the unified flag, so enabling them is a `v2-api` decision now. Reverts the deploy modal, copilot handlers, and api_trigger example to the v1 execute endpoint: v1 works unchanged, and the UI must not advertise a surface most users would get a 404 from. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz * fix(executor): restore child-cost aggregation dropped by the staging merge Staging's custom-block rewrite deleted `aggregateChildCost` from workflow-handler.ts, and git merged that file cleanly — but this branch's workflow-tool-runner.ts, added for the v2 execute migration, still imports it. A silent semantic conflict: no marker, broken build. Taking staging's rewrite is correct, so the helper is defined locally in its one remaining consumer rather than resurrected in the file staging just rewrote. Same four lines over the still-exported `calculateCostSummary`, so a failed child workflow keeps billing the hosted-key spend it consumed instead of reporting $0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(tables): make lib/table/orchestration the single implementation (#6134) * refactor(orchestration): move the shared error contract out of lib/workflows OrchestrationErrorCode and statusForOrchestrationError are the contract every lib/[resource]/orchestration module returns against, but they lived inside the workflows module, so resource-neutral code (lib/folders) already had to import from a workflow path. Moved to lib/core/orchestration/types. Adds a 'locked' class mapping to 423. Both tables and workflows have a lock that forbids a mutation, and each caller was translating that to a status itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(tables): make lib/table/orchestration the single implementation Column update was implemented four times — the UI route, v1, v2, and the copilot table tool — each calling the same column services but owning its own guards, error mapping, and audit. The copies had drifted, and the drift was the bug: v2 was missing both guards, only the copilot copy minted stable option ids, and only v1/v2 audited. performUpdateTableColumn, performDeleteTable, and performDeleteTableRow now own that logic; all ten call sites reduce to auth, parse, call, render. The guards are asserted once in lib/table/orchestration rather than four times against four routes. Behavior this consolidates, previously true on only some paths: - The typeChanging guard. updateColumnType early-returns on an unchanged type and drops any options sent with it, so restating the current type alongside new options silently discarded them. v2 had no guard at all and, since its contract shares v1's body schema, accepted options and ignored them. - The select-unique guard. Each write is its own locked transaction, so a rename or type change paired with a constraint write that is going to fail commits first and then throws, half-applying the schema change. - Stable select-option ids. Cells reference the option id, so an edit that re-sends an option by name has to reuse it or every cell holding it is orphaned. Only the copilot path did this; normalizeSelectOptionsInput moves to lib/table/select-options and now covers every caller. It preserves a supplied id, so it is a no-op for the fully-formed options the HTTP contracts accept. - required forwarded into the type and options writes, so a conversion validates against the constraint the same request is setting. - An audit on every successful update. The UI route and the copilot tool emitted none. - Single-row delete through the row service. v2 did a raw db.delete, skipping assertRowDelete and deleteOrderedRow, so a delete-locked table returned 200 and the row-count bookkeeping never ran. - The delete actor handed to deleteTable, which audits only when a row was actually archived. v1 and v2 omitted it and audited themselves outside that check, emitting TABLE_DELETED for a no-op delete of an archived table. Failure classes come back as OrchestrationErrorCode; v2 renders them through a new v2ErrorForOrchestration, mirroring statusForOrchestrationError on the v1 and UI surfaces, so a given failure maps to the same status everywhere. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(tables): bind the column-update tests to the orchestration function The base's route tests assert which column service each payload reaches — the behavior that now lives in performUpdateTableColumn. They mocked the `@/lib/table` barrel; the orchestration module imports the service directly, so they mock that too and keep asserting the same thing through the extracted implementation. The orchestration tests move onto the base's semantics: writes address the stable column id, a rename rides inside the write it accompanies rather than running first, and the currency guards replace the non-select options guard the service now owns. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(copilot): drop the column-type import the delegation made dead Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(tables): move the audit log out of the table service `lib/table/service.ts` wrote its own audit rows, so whether an operation was audited depended on which function a caller reached for rather than on a user having performed it. That is what let v1 and v2 audit a no-op delete, and what made `deleteTable`'s optional `actingUserId` double as an audit opt-out flag. Worse, most sites fell back to `actingUserId ?? createdBy`, so an unattributed call was logged against the table's *creator*. The copilot `mv` path passed no actor at all: renaming someone else's table recorded them as the renamer. Audit now lives in the orchestration functions — performDeleteTable, performRenameTable, performMoveTableToFolder, performUpdateTableLocks — and the services just write. Internal callers (folder cascade, import rollback) keep calling the service and are silent by construction rather than by remembering to omit an argument. Two services now return what the audit needs: `deleteTable` reports whether it actually archived a row, so a repeat delete logs nothing; `updateTableLocks` returns the before/after locks, since only the locked write can observe the transition its description names. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(tables): restore audit provenance and conflict status in orchestration Moving the audits into the orchestration functions dropped three things the routes had been carrying, and added one the orchestration now owns twice. - The v1 and v2 column-update routes passed `request` to `recordAudit`, so their audit rows recorded the caller's IP and user-agent. The orchestration function had no way to receive it. Every table orchestration function now takes an optional `OrchestrationRequestContext` and every HTTP route forwards it; the copilot and VFS callers, which have no request, omit it. - `classifyTableMutation` matched `TableConflictError` on "already exists" appearing in the message and reported it as `validation`, turning the UI route's 409 on a duplicate table rename into a 400. It now matches the type, the way `performRestoreTable` already did. - `captureServerEvent` ran on every delete while the audit was gated on a row actually being archived, so a repeat delete of an archived table still reported `table_deleted`. Both now hang off the same evidence. - The copilot delete path kept its own `captureServerEvent` from when the service did not emit one, double-counting every copilot table delete. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YGzbVDZpe2dEALbu2BUU8a * fix(tables): say which type a no-op column update restated A copilot `update_column` payload whose only content was the column's current type used to return success with the live schema, while the v1, v2, and UI routes rejected the same payload with "No updates specified". Delegating to `performUpdateTableColumn` unified them onto the routes' rejection — correct, but the message tells the caller its request was empty when it named a type. The orchestration function now reports the same thing `updateColumnType` reports when it loses this race concurrently: the column is already that type, re-issue without the type change. An empty payload still reads "No updates specified". Drops the copilot's `outcome.table ?? tableForUpdate` fallback with it — the comment described the no-op that can no longer reach that line, and a success always carries a table. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YGzbVDZpe2dEALbu2BUU8a * refactor(tables): classify failures by type instead of by message text The table module decided HTTP statuses by searching error messages for phrases. `VALIDATION_MESSAGE_FRAGMENTS` and `ROW_WRITE_ERROR_PATTERNS` held 32 substrings between them, and fifteen more lists were inlined in routes — 83 matchers over 17 files, each its own copy of the guesswork and already drifted apart. It made message wording load-bearing: `TableRowLimitError`'s own doc comment noted that its text had to contain "row limit" for a route to answer 400, and adding "already exists" to a rename message silently demoted a 409 to a 400 (the bug fixed one commit ago, by adding another special case). Services now throw `OrchestrationError`, which carries the transport-neutral `OrchestrationErrorCode` the layers above already speak. Classification is one `instanceof` in `orchestrationErrorResponse` (UI + v1) and `v2CaughtOrchestrationError` (v2). Every pattern list is gone. Wording is free to change; an unclassified error still becomes a generic 500, which is what an unexpected fault should be. `asOrchestrationError` walks the `cause` chain rather than testing the caught value directly: drizzle wraps a throw raised inside a transaction callback in a `DrizzleQueryError` whose own message is the failed SQL, so a bare `instanceof` would drop every failure raised inside `withLockedTable`. That is the same reason `rootErrorMessage` had to dig for a root cause before. Three throws stay bare `Error` deliberately — `Table ID mismatch`, `Workspace ID mismatch`, and `Failed to build upsert conflict predicate` are internal invariants no consumer classified, and they keep falling through to a 500. `Insufficient capacity` was in the pattern list with no producer anywhere in the codebase. Status changes, all deliberate: - `'forbidden'` joins the code union so the table-row-limit ceiling keeps its 403; without it this refactor would have flattened it to 400. - import-async's table-limit rejection: 400 -> 403, matching the two other create routes it had drifted from. - Renaming a table to an invalid name: 500 -> 400. `validateTableName` messages don't contain "Invalid", so no matcher ever caught them. - Restoring a table that isn't archived, or into an archived workspace: 500 -> 400. - A duplicate *column* name stays `validation`/400 rather than becoming a 409 like a duplicate table name. Both v1 and the orchestration have always answered 400 for it; changing a published status is not this refactor's job. The twelve tests that changed were asserting the substring mechanism itself, constructing plain `Error`s with magic strings. They now assert the real contract, plus new cases pinning that identical wording carrying no classification stays internal and keeps its message off the wire. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YGzbVDZpe2dEALbu2BUU8a --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * feat(api): add v2 endpoints for MCP servers, skills, custom tools, folders, and credentials (#6150) * feat(api): add v2 endpoints for MCP servers, skills, custom tools, folders, and credentials * fix(api): correct credential role, skill permission bar, MCP url identity, and custom-tool conflict mapping * fix(api): align credential mutation gating, provider-outage status, and unique-violation conflicts * fix(api): close unique-violation, revival, orphan-write, and env-rename gaps * fix(api): treat every provider-outage code as unavailable on create and update * fix(credentials): use the shared outage predicate on the session update path * fix(contracts): anchor the predicate double-cast annotation to the cast `check:api-validation:strict` counted 9 unannotated double-casts against a baseline of 8, failing CI. The predicate leaf schema was annotated, but the annotation sat above the declaration while the checker anchors on the line carrying the cast — five lines below, at the close of the object literal. The scanner walks back at most three lines and stops at the first non-comment one, so it hit `value: z.unknown().optional(),` and never saw the reason. Splitting the object schema from the cast puts them adjacent, so the existing reason binds. No behavior change — the cast, the schema, and the reasoning are unchanged. Also lowers the rawJsonReads ratchet 6 -> 5 to match the current count, which had drifted down; leaving it high lets a removed raw read silently come back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(skills): point the orchestration error contract at its moved module #6150 branched before #6134, so skill-lifecycle.ts imports @/lib/workflows/orchestration/types — the module #6134 moved to @/lib/core/orchestration/types. Git merged a file deletion on one side with a new file referencing it on the other: no textual conflict, broken build. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(knowledge): make lib/knowledge/orchestration the single implementation (#6154) * refactor(knowledge): make lib/knowledge/orchestration the single implementation Knowledge base create was implemented four times — the internal route, v1, v2, and the copilot tool — and the orchestration around the shared write had drifted. Extract it the same way lib/table/orchestration was: services write, orchestration decides which writes run, guards them, audits them, and returns a transport-neutral failure. Behavior converged, not preserved: - One chunking default (DEFAULT_CHUNKING_CONFIG). The agent defaulted minSize to 1 against the API's 100, so identical input produced differently-chunked knowledge bases depending on who created it. The agent path now chunks at 100. - Every successful mutation is audited inside the orchestration function. The copilot tool called recordAudit zero times, so agent-created knowledge bases, document uploads, updates and deletes left no audit trail at all. - Failures classify by class, not by message text. The knowledge service errors are OrchestrationError subclasses and storage-quota rejections throw a shared StorageLimitExceededError, replacing four separate message greps for "already exists" / "does not have permission" / "storage limit". delete_connector reported the opposite of what happened. It reached the route through an internal HTTP self-call that sent no query string, so the route's keep-documents default always applied while the agent told the user the documents had been removed. The self-call is gone — all four connector operations run in-process — and the orchestration returns the real counts. Also: - OrchestrationErrorCode gains 'payload_too_large' (413 / PAYLOAD_TOO_LARGE). Without it, dropping the storage-limit message match would have regressed the documented 413 on knowledge base create and document upload to a 500. - messageForOrchestrationError renders a route's own wording for an unclassified fault, so a driver's message no longer reaches the client on a 500. - v1 and v2 knowledge base update now forward actorUserId, which the service requires for a workspace move; both omitted it. - The connector DELETE route reads deleteDocuments through parseRequest. Its contract declared z.boolean(), which would have rejected the string a query param actually is. - Drop the 409 from POST /api/v2/knowledge/{id}/documents in the OpenAPI spec. Nothing on the upload path throws a conflict; it was only ever reachable by the message match this change removes. Behavior change worth noting: a v1/v2 PUT carrying only the workspaceId scope field and no actual updates now returns 400 rather than 200 with the unchanged knowledge base. Deliberately deferred: document update remains internal-only. Extracting performUpdateKnowledgeDocument makes exposing it on v1/v2 a contract and a route away, but that is a new public surface rather than part of this consolidation. * fix(knowledge): make connector create atomic and stop flattening failures Review round 1 on #6154. - Resolve the billing payer before the connector is committed, not after. A malformed attribution header rejected post-commit left a live connector behind a 500, and a retry created a duplicate plus duplicate sync work. Manual sync resolves before writing its audit for the same reason. - Let the source-config validator carry its own failure class. Collapsing every rejection to `validation` flattened the connector PATCH route's 401 (stale stored credential) and 409 (missing workspace context) into a 400. - Add `unauthorized` to OrchestrationErrorCode. It is the class that 401 was already expressing on this route, and the v2 vocabulary already had UNAUTHORIZED; only the shared union was missing it. - Report a knowledge base that exists but failed to archive as failed, with the reason, rather than as not found. The copilot delete loop folded every non-not-found failure into `notFound`, telling the user it was never there. - Route copilot failures through the same message helper the HTTP surfaces use, so an unclassified fault's raw text (a driver's failed SQL) no longer reaches the agent verbatim while the UI and public APIs get the generic wording. * feat(api): expand the public v2 files surface (#6160) * feat(api): expand the public v2 files surface Adds folder support, rename/restore, move, bulk archive, share, and content replace to /api/v2/files, so managing files by API no longer stops at upload + download + archive-one. Routes are thin: auth -> parse -> perform* -> serialize. Share and content replace get their orchestration extracted first so the session routes and the public ones cannot diverge on the effective-authType resolution, the EE public-sharing gate, or the storage-quota classification. Presigned upload stays session-only: presign does an advisory quota check and the real debit happens in the separate register step, so a caller that never registers leaves unaccounted bytes with no reaper. The buffered multipart path debits inside uploadWorkspaceFile's own transaction. * fix(files): classify folder and content failures instead of 500ing them Bugbot round 1. The v2 routes map errorCode straight to a status, so every manager failure that arrived unclassified became a 500 for what is really a caller-fixable 400 or 404. - Folder manager throws OrchestrationError: missing target/folder -> not_found, reparent cycle / self-parent / restore-into-archived-workspace -> validation. - File manager does the same for the in-transaction 'File not found' paths that the earlier pass missed. - updateWorkspaceFileContent's outer catch re-wrapped everything in a bare Error, which stripped the class off StorageLimitExceededError and the new not_found alike. It now rethrows a classified failure untouched and attaches cause to the generic wrap, so asOrchestrationError can still walk the chain. - Every remaining perform* gained the asOrchestrationError branch. - renameWorkspaceFile returned the pre-update read, so the v2 PATCH reported a stale updatedAt; it now returns the timestamp it actually wrote. Docs: upload auto-suffixes a duplicate name rather than rejecting it, matching the in-app uploader. The description claimed 409 and was simply wrong. * fix(files): surface a failed upload read-back as the real error getWorkspaceFile swallows a query failure and returns null unless throwOnError is set, so a transient blip on the post-upload read reported as 'file could not be read back'. Distinguish the two: a real null after a just-committed write is an invariant break, a query failure is itself. * revert(api): drop the dedicated v2 file-folder routes File folders already live in the shared folder table as resourceType 'file' (#6045 cut them over, #6051 dropped workspace_file_folders), and the remaining file-specific folder machinery is being folded into the generic folder engine. Publishing /api/v2/files/folders/** would pin that transitional split into a public contract we'd then have to keep or break. Files stay folder-aware — folderId/folderPath on the projection, folderId on upload, and the move route — because a folder id is a folder.id and survives the unification untouched. Folder management belongs on /api/v2/folders once that surface serves resourceType 'file'; until then there is no v2 way to enumerate file folders, which is the deliberate gap. The orchestration classification fixes stay: the internal routes and the copilot file-folder tools still call those perform* functions. * fix(files): classify upload failures instead of matching their wording Bugbot round 2. uploadWorkspaceFile had the same outer-catch rewrap that updateWorkspaceFileContent did, so a blown storage quota reached the route as a bare Error and the v2 handler recovered the status by substring-matching the message. Any rewording silently demoted a 413 to a 500. - uploadWorkspaceFile rethrows a classified failure untouched and attaches cause to the generic wrap. - FileConflictError is now an OrchestrationError('conflict'), so a duplicate name classifies like every other conflict. Its 'FILE_EXISTS' discriminator had no readers and is gone; the instanceof checks elsewhere still hold. - The v2 upload handler uses v2CaughtOrchestrationError, dropping all three string matches. Also documents that bulk-archive is best-effort: unknown or already-archived ids are skipped rather than failing the call, and deletedItems is what actually happened. That asymmetry with the single-id DELETE was undocumented. * feat(api): add search, filtering, and sorting to the v2 list endpoints (#6189) * feat(api): add search, filtering, and sorting to the v2 list endpoints One convention across every v2 list, documented on lib/api/contracts/v2/shared.ts: `search` (case-insensitive substring on the resource's natural name field), `sortBy` + `sortOrder` (per-resource enum, never a free string), and enumerated resource-specific filters. Reuses the sortBy/sortOrder pair v2 logs and v2 knowledge-documents already ship rather than inventing a third dialect alongside the Logs filters and the Tables predicate grammar. Every filter and sort is pushed into SQL. GET /api/v2/files previously read the whole scope and sorted/sliced it in JS; it now goes through a new queryWorkspaceFiles that filters, orders, and bounds the page in one query. Cursors are stamped with the sort they were minted under, so replaying one under a different sort is a 400 instead of silently duplicated or skipped rows. * fix(api): validate v2 cursor key values and compare timestamps at ms precision Two review findings, fixed at the root by making a keyset key own its cursor codec instead of hand-writing a decoder per sort. Cursor key values are caller-controlled, and matching the sort stamp and key count was not enough: an unparseable timestamp or a non-numeric size reached the query as an Invalid Date or NaN and surfaced as a 500. Each key now type- checks its own value and rejects a cursor it cannot hold, which both routes render as the documented 400. Timestamp keys now order and compare on date_trunc('milliseconds', col). Postgres keeps microseconds and defaultNow() populates them, but a cursor value round-trips through a millisecond-only JS Date — comparing the raw column against the truncated value re-admitted the page's own last row, duplicating it and stalling pagination outright at a page size of one. Reachable today via workspace_files.updated_at, which insertFileMetadata leaves to defaultNow(). * feat(api): complete the v2 workflows resource with versions and CRUD (#6184) * feat(api): complete the v2 workflows resource with versions and CRUD Adds version listing/detail plus create, update, and delete to the v2 workflows surface, which previously covered only execution and deployment. - GET /api/v2/workflows/[id]/versions — cursor-paginated, newest first - GET /api/v2/workflows/[id]/versions/[version] — version + pinned state - POST /api/v2/workflows, PATCH and DELETE /api/v2/workflows/[id] All six delegate to the existing orchestration and persistence helpers; no new domain logic. * fix(api): check folder containment before lock state; reject malformed version cursors assertFolderMutable walks a folder's ancestor chain without filtering on workspace, so inspecting it before containment let a caller tell a locked folder in someone else's workspace (423) from a nonexistent one (400). Create and update now assert containment first, matching the ordering import-workflow.ts already uses. A version cursor that decodes to JSON without a numeric version filtered every row out and returned an empty page with nextCursor null, which reads as a clean end-of-list. Malformed cursors are now a 400. * refactor(api): page workflow versions in the persistence helper listWorkflowVersions read every version row and the route filtered and sliced the result in memory, so the response was bounded but the query was not. It now takes optional limit/afterVersion, turning the cursor into a real keyset query; the route asks for limit + 1 and only trims the has-more probe. Both params are optional, so the internal, v1 admin, and copilot callers are unchanged. Also restores the untouched GET handler in [id]/route.ts to its original formatting — collapsing its signature had re-indented the whole body and buried the actual additions in whitespace churn. * feat(api): expand v2 tables with stateless multipart transfers (#6188) * feat(api): expand the public v2 tables surface Adds 16 operations so a v2 caller can do what the internal surface can: rename/move/lock a table, restore it, manage saved views, run enrichment columns, look up rows, and import/export with observable job control. Extracts lib/table/orchestration/import.ts (performTableCsvImport, performCreateTableFromCsv) and lib/table/export-stream.ts from the first-party routes, then repoints those routes at them, so v1 and v2 cannot drift on what an import or export actually does. events/stream, metadata and dispatches stay internal — they are editor state, not public API. * fix(api): make v2 table PATCH all-or-nothing and name the lock in every 423 Greptile P1: PATCH applied locks, rename and move as three sequential transactions, so a folder rejected mid-request left the earlier writes persisted while the response reported failure — and the schema-changed signal was skipped, leaving open clients on stale state. Every rejectable condition now runs before the first write, and the signal fires whenever anything did land. Cursor: v2TableLockError dropped the lock kind, so async import, column run, enrichment and table mutations returned a bare LOCKED. A table has four independent locks, so the caller could not tell which to clear. * fix(api): report the lock kind on classified 423s too, not just thrown ones The previous commit named the lock only where the rejection was thrown and caught at the route boundary. Where it instead arrives as a classified `errorCode: 'locked'` outcome — delete table, delete row, update column, and the table mutations — the kind was dropped, so those 423s stayed unactionable while their neighbours improved. The orchestration results now carry `lock`, and a shared `v2TableOrchestrationError` renders both arrival paths into the same `{ code, message, details: { lock } }` body. `details` is omitted rather than sent null when the kind is unknown, so a caller branching on it sees absence instead of a phantom value. * fix(api): make async table imports observable, not just startable `POST /import-async` pointed callers at `GET /api/v2/tables/jobs` to track progress, but that endpoint filters to `type = 'export'` — imports are derived onto the table itself, one write job at a time, and exports get a separate list precisely because they are excluded from that derivation. The public Table shape omitted those derived fields, so an async import could be started and cancelled but never observed to completion, failure, or progress. That is the gap the import/export/job-control set was meant to close. Table now carries `job` — id, type, status, rowsProcessed, error, or null when idle — and the import-async docs point at the table rather than the export list. * feat(api): make v2 table PATCH state which operations landed on failure Greptile held the PR at 4/5 on the residual non-atomicity and named two acceptable resolutions: make PATCH atomic, or have the contract adopt and expose partial-success explicitly. Atomicity would mean threading one transaction through renameTable, moveTableToFolder and updateTableLocks — three shared service functions with four non-test callers including the first-party route and two copilot tools — and deferring their per-operation audits to commit time. That is a refactor of shared write paths well outside this PR. So the contract states it instead. Every rejectable condition is already pre-validated, so a failure here is a genuine fault; when one follows a successful operation the error now carries `details.applied` listing what is live. Absent when nothing applied, so its presence always means "these changes took effect despite the error". Documented on the operation. `v2ErrorForOrchestration` gained the optional `details` this needs. * fix(api): make table lock flags read-only on the public v2 surface The new PATCH /api/v2/tables/[tableId] accepted a `locks` object, gated on workspace admin plus the table-locks feature. That still lets an API key clear the guard placed there to stop it: `write` is the floor for the endpoint, and admin keys are ordinary API keys, so a lock is no longer a boundary the key cannot cross. Locks stay readable on the table resource and enforcement is unchanged (a locked verb still returns 423). Changing one is now a first-party admin action only. The v2 body is declared here rather than reusing the first-party updateTableBodySchema, which keeps its `locks` field so the UI can still toggle them. It is .strict(), so a request carrying `locks` is rejected with a 400 naming the field instead of silently succeeding without applying it. * fix(api): keep reporting applied operations when the PATCH re-read fails The composite table PATCH promises that `error.details.applied` names the operations that are live despite an error, but `applied` was scoped inside the try. A rename or move that committed and was then followed by a throw in the final re-read — or a re-read finding the table archived — returned a bare 500/404 with no details, telling the caller nothing had landed. It would then retry into a duplicate-name conflict or repeat the move. `applied` is now function-scoped so every post-write exit carries it: the 404 on a missing re-read, a thrown lock error, a classified orchestration error, and the generic 500. `v2TableLockError` gains the same `extraDetails` parameter `v2TableOrchestrationError` already had. * feat(api): add workflow group writes to the v2 tables surface v2 exposed GET /groups but none of the writes, so the public API could run an enrichment or workflow column and read its binding, but never create one. A caller could add a plain data column and trigger the machine; wiring the two together still required the UI. Adds POST/PATCH/DELETE on /api/v2/tables/[tableId]/groups. The group is the unit that fills columns — one group feeds several — so creating one creates its output columns in the same call, matching the first-party shape rather than inverting it onto the column endpoint. Four departures from the first-party body, all public-surface concerns: - group.id is optional and server-generated. The UI mints an id to render optimistically; a public caller has no such need and a client-chosen id is a collision waiting to happen. - outputColumns[].workflowGroupId is dropped from the body and stamped from the resolved group, so it cannot disagree with it. - autoRun defaults to false. First-party defaults true so a UI add fills cells immediately; here it would make one POST fan out a metered run across every existing row. - A group naming neither a workflowId (type manual) nor an enrichmentId (type enrichment) is a 400 rather than a half-specified group the route has to guess about. Also rejects an outputColumns entry no group output feeds — the two arrays are joined by column name, and the first-party client builds both from one picker so it cannot desync, but a public caller can. Workspace containment on workflowId is asserted before it is persisted, on create and on any update that re-points the group; without it a table becomes a way to invoke workflows the key cannot otherwise reach. * improvement(api): make v2 table import and export async-only Drops the three synchronous entry points: POST /tables/[tableId]/import, POST /tables/import-csv, and GET /tables/[tableId]/export. Sync import tied a write to the lifetime of an HTTP request. The body *was* the data, so it carried a 10 MB cap that Next silently truncates past — a partial import reporting success. It also had no job, so a timeout mid-write left rows in place with nothing to poll and nothing to cancel. The async path reads the file from storage instead: upload via POST /api/v2/files for a key, start with POST /import-async, watch GET /tables/[tableId] -> job, stop with POST /job/cancel. Sync export carried no such hazard, but one shape per operation beats two: with both removed the surface has exactly one way to move a table in or out, and the CLI wraps the extra calls. This also removes the last multipart handling in v2 tables. Those were the only routes bypassing parseRequest — form fields were parsed by hand against separate form schemas, outside the contract system every other v2 write goes through. Create-a-table-from-CSV is now two calls: POST /tables, then /import-async with createColumns. csvImportModeSchema is append|replace, so there is no single-call create. Route baseline 1064 -> 1061. * docs(api): correct the import-async note about upload size limits The docstring claimed there is no synchronous upload endpoint and so no request-body size cliff. Both are wrong: POST /api/v2/files is a synchronous multipart upload with a 100 MB cap, and it is the only v2 upload path (presigned is deliberately absent). What async-only actually bought: the cap went 10 MB -> 100 MB, it fails on an explicit size check and a bounded body read rather than a proxy cap that silently truncates, authorization completes before any body is buffered, and the table write is a job that can be watched and cancelled. * feat(api): unify file and table transfers * improvement(api): make multipart transfers stateless * fix(api): make table import completion retries idempotent * feat(v2-tables): paginate the table list `GET /api/v2/tables` returned every table in the workspace in one response — it used the cursor envelope but hardcoded `nextCursor: null`, and had no `limit`. That was defensible when tables were only created through the UI; `POST /api/v2/tables` is public now, so a script can create them in bulk and the list has no way to ask for less. Adds `queryTables` alongside `listTables` rather than changing it, so the internal callers that genuinely want the whole scope are untouched — the same split `queryWorkspaceFiles` / `listWorkspaceFiles` already uses. Filter, order and slice all run in the query, so a `search` never costs a full-workspace read. A cursor whose values don't bind raises a validation error instead of being coerced to "no filter", which would have silently served page 1 under a resumed cursor. The keyset closes on `id` so a page boundary inside a run of equal names or timestamps stays stable. The shared `LimitQuery` doc component said "Maximum rows to return"; it now serves the table list too, so the wording is resource-neutral. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(api): add multipart knowledge document uploads * fix(api): keep usage admission at knowledge upload session creation * feat(knowledge): wire knowledge base uploads to multipart sessions * fix(knowledge): refuse to abort an upload once a document is bound * fix(uploads): prevent multipart cleanup races * Unify file creation and signed upload sessions (#6264) * feat(uploads): unify signed upload sessions * fix(uploads): preserve attachment storage semantics * feat(files): add authored file creation * fix(uploads): omit hoisted S3 metadata headers * feat(api): add file metadata endpoint * improvement(api): scope folders to resource paths (#6284) * improvement(api): scope folders to resource paths * fix(files): serialize folder resolution with uploads * fix(files): release folder lock before upload setup * fix(api): normalize folder paths and unblock resource mutations * fix(api): make resource cleanup and metadata consistent * improvement(uploads): persist multipart sessions in postgres * fix(db): store table row trigger timestamps in UTC * improvement(api): default folder deletion to non-recursive * fix(billing): unify chat usage source * improvement(logs): expose trace spans on log detail * fix(logs): parse list trace spans * improvement(api): replace workflow jobs with execution resources (#6294) * improvement(api): replace workflow jobs with execution resources * fix(api): preserve legacy jobs while preferring v2 executions * fix(api): make execution polling resume-aware * fix(ui): hide async examples for public workflows * fix(api): bridge resume queue visibility lag * feat(api): add v2 workflow resume endpoint * fix(api): project pending resume attempts * fix(api): prefer terminal logs over stale resumes * improvement(api): unify v2 resource query layers (#6319) * improvement(api): unify v2 resource query layers * fix(api): address v2 review findings * fix(api): preserve cancelled queue status * fix(api): guard cancelled job transitions * fix(api): close v2 resume and log gaps * feat(api): rename v2 executions to runs * feat(api): split credentials and secrets * feat(api): add workspace metadata and email attribution * improvement(api): consolidate public v2 route handling * improvement(files): centralize operations across APIs and Copilot (#6392) * improvement(files): unify rename authorization * chore(skills): add file operation migration guide * improvement(files): consolidate file operation authorization * improvement(files): extract shared operation foundation * improvement(api): simplify internal route declarations * improvement(files): centralize application authorization * refactor(api): share workspace file name validation * refactor(files): centralize copilot application calls * docs(skills): generalize application operation migration * improvement(api): centralize remaining v2 resource operations (#6412) * improvement(api): centralize v2 resource operations * fix(api): preserve custom tool conflict errors * improvement(api): migrate policy-sensitive v2 reads (#6410) * improvement(workflows): centralize v2 application operations (#6411) * refactor(api): migrate v2 knowledge operations (#6413) * refactor(api): migrate v2 knowledge operations * fix(knowledge): fail upload completion on dispatch errors * fix(knowledge): preserve upload retry and VFS errors * improvement(tables): centralize v2 application operations (#6414) * improvement(tables): centralize v2 application operations * fix(tables): preserve run validation and signals * feat(auth): add scoped internal executor delegation (#6459) * feat(auth): add scoped internal executor delegation * fix(auth): derive delegation lifetime from one timestamp * Include share status in file metadata * feat(auth): centralize delegated identity policy (#6462) * improvement(copilot): consolidate application adapters (#6450) * improvement(api): harden application route boundaries (#6451) * improvement(api): harden application route boundaries * fix(folders): reject creates at workspace cap * fix(knowledge): enforce trusted workspace scope (#6452) * fix(knowledge): enforce trusted workspace scope * refactor(knowledge): declare v2 body lifecycle * finish knowledge application migration * refactor(knowledge): compose copilot batch commands * fix(knowledge): parse connector query flags * fix(knowledge): finalize partial batch effects * fix(knowledge): align merged application boundaries * fix(knowledge): close application boundary review gaps * style(knowledge): satisfy branch biome checks * fix(knowledge): page connector documents in editor * refactor: enforce Copilot table application boundary (#6453) * refactor: enforce copilot table application boundary * fix(tables): finish application boundary migration * fix(tables): restore scoped copilot imports * fix(tables): compose copilot commands atomically * fix(tables): preserve workflow group scheduling * fix(tables): complete fixed copilot composition * fix(tables): reject enrichment output mutation * fix(tables): complete authorized application boundary * fix(workflows): migrate Copilot application boundary (#6455) * fix(workflows): migrate Copilot application boundary * fix(workflows): finish delegated application migration * fix(workflows): encode VFS folder aliases * fix(workflows): close application composition gaps * fix(workflows): preserve VFS validation errors * fix(workflows): complete application boundary migration * test(workflows): format canonical binding coverage * fix(workflows): scope executor metadata reads * fix(workflows): bind executor metadata targets * improvement(skills): align application operation guidance (#6532) * feat(api): expose v2 resource owners * fix(api): distinguish visible resource authorization failures (#6537) * feat(api): generate v2 OpenAPI from contracts (#6509) * feat(api): generate v2 OpenAPI from contracts * fix(api): preserve string boolean wire defaults * fix(api): document file download headers * fix(docs): use TypeScript CLI with Next.js * fix(docs): avoid client-rendered theme script * fix(api): document departed audit default * feat(api): replace legacy core docs with v2 * feat(api): generate v2 OpenAPI from contracts * feat(api): refine generated v2 OpenAPI docs * fix(docs): align localized v2 execution examples * fix(ci): restore Helm diff and sync audit mock * fix CI regressions after staging merge --------- Co-authored-by: Waleed <walif6@gmail.com> Co-authored-by: Theodore Li <theodoreqili@gmail.com> Co-authored-by: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com> Co-authored-by: Theodore Li <theo@sim.ai> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(executor): restore delegated workflow execution * fix(executor): trust custom block execution scope * fix(providers): keep tool parameters type safe
…them (#6536) * fix(chat): deduplicate chat sends server-side instead of probing for them A client cannot tell whether a request it aborted reached the server: the chat route never reads `request.signal`, so an accepted one still opens the chat, persists the user message, and bills the turn after the socket drops. #6525 answered that by polling the orphaned stream before retrying — a 2.5s guess that had to distinguish "no such stream" from "we stopped looking", and still left a window open. The codebase already owns the right tool. `IdempotencyService` backs webhook, polling, and billing dedup, and `billingIdempotency` exists for exactly this hazard: "a retry would double-record usage — real money". Chat sends now claim the same way, keyed on the client-generated `userMessageId` and scoped to the caller so nobody can probe another user's sends. A repeat gets 409 naming the chat the first attempt opened — deliberately the shape the pending-stream lock already returns, so the client's existing conflict handler reattaches instead of starting a turn, with only the chat-adoption line added. The claim fails open at every step. Deduplication saves a duplicate chat; the send IS the user's message, so an unreachable bookkeeping store degrades chat rather than taking it down. It is released when a send fails before recording a chat, and deliberately kept once recorded. Retrying now just reuses the id, which deletes the probe outright: the poll and its two constants, the three-state result, the epoch plumbing that kept a superseded poll from re-sending, and the chat-adoption branch it needed. The client hook nets 67 lines smaller. Idle sends go back to calling `startSendMessage` directly. #6525 routed them through the durable queue so recovery had a backing entry, which put every message in the product through the queue store, sessionStorage, and the dispatch loop for the sake of a rare path — and the recovery never needed it, since the message, attachments, contexts, and id are all in scope at the abort. Both callers now share one `handOffWithdrawnSend`. `startSendMessage` takes its optional tail as an options object; it was at six positional parameters and the retry id would have been a seventh. Tests cover both halves: the server dedups, scopes the key per user, records the chat, and still sends when the claim store is down; the client reuses the original id on retry and adopts the chat a deduplicated retry names. Each was confirmed red without its fix. * fix(chat): keep a withdrawn send in its own chat, and release stranded claims Audit follow-ups, two of them real defects in the previous commit. A withdrawn send routed unconditionally through the cross-surface lanes. Those deliver to whatever chat is mounted next, so sending in one chat and switching to another re-sent the message into the second one. The dispatcher already drew the distinction; the idle path now draws it too — a chat-bound key is the stable chat id, so re-queueing under it both retries durably and keeps the message where the user put it. Only a chatless key, which dies with its mount, goes to the lanes. The claim release sat in `catch`, so the two paths that return a response without throwing — a rejected branch, and a missing chat — stranded an in-progress claim for its full 60s TTL, and a retry inside that window got a spurious "already sent" instead of the real error. Moved to `finally`. Also: `userMessageId` is now length-bounded, since it becomes part of a Postgres key and an oversized one would throw inside the claim; `requestId` was still empty at claim time, so both dedup logs printed a blank prefix; the provider segment said `mothership` on a handler that also serves the workflow copilot, and now says what the key identifies; `retryFailures` was dead config, only read by `executeWithIdempotency`, which this caller never invokes; the doc pointed at `billingIdempotency`, which has no consumers, and now points at the live Stripe analogue. Trimmed: `sendClaimRecorded` folded into clearing `sendClaim`, the unread `kind` discriminant dropped from a one-arm union, the single-use `claimedChatId` inlined, and the prose on all three of those cut back to what the code does not already say. * fix(chat): make a send's claim permanent only once its turn starts The claim became permanent as soon as the chat resolved, but three exits still return without starting a turn — a rejected branch, a missing chat, and a pending-stream collision. The last one matters: the queued-send-handoff path deliberately retries under the original `userMessageId` after a collision, and against a permanent claim that retry deduplicated to a chat whose turn never ran, reattaching to a stream that does not exist. A send that had merely collided became unsendable for the claim's full hour. The claim is now dropped immediately before the stream response is returned, so `finally` releases it on every other exit. Recording the chat still happens as early as possible — a concurrent duplicate needs somewhere to go — it just no longer implies the turn happened. * refactor(chat): give the send claim a single point of permanence Recording the chat also dropped the claim when it failed, which left a second way for a claim to stop being tracked and a compound hole behind it: a failed record followed by a throw stranded the claim for its in-progress TTL, and a retry inside that window reattached to a turn that never started. Only one line now decides permanence — the claim is cleared immediately before the stream response — so `finally` releases it on every exit that did not start a turn, including a failed record. The `recorded` flag is gone with it. Covers the 400 early return with a release assertion: that path returns without throwing, so it is the one that proves the release has to live in `finally`.
* fix(v2-api): stop leaking resolved secrets in logs and serving doc source Two regressions shipped with the v2 API (#5273) where v2 diverged from the v1 path it replaced, plus the hardening that fell out of auditing them. **v2 logs bypassed secret redaction.** `getPublicLog` and `listPublicLogs` called raw `materializeExecutionData`, while every other reader — v1 list and detail, CSV export, `fetch-log-detail`, both data-drain sources — calls `materializeExecutionDataForDisplay`, which applies the resolved-secret provenance projection. Both v2 routes then serialize `traceSpans` and `finalOutput` straight onto the wire, so unredacted secrets could reach the public API. Swapped to the display projection and threaded the principal's subject user into the read context. **v2 file download served generation source.** `GET /api/v2/files/{fileId}` streamed `file.key` raw. AI-generated docs store their generation source as the primary file, so a raw download yields source text under a `.pdf` name — a file the recipient cannot open. Generated docs now resolve to their compiled artifact; ordinary uploads still stream and are never materialized, gated on the recorded generation-source type rather than the extension. The resolve is capped at MAX_RENDERED_DOCUMENT_BYTES, and a still-compiling artifact returns a retryable 409 rather than a 500. Also in this change: - Reconcile the two v2 verbs that used PUT for PATCH semantics: `PUT /v2/knowledge/{id}` and `PUT /v2/tables/{tableId}/rows` are both all-optional partial updates. Breaking for API-key clients, but the surface is dark-launched behind the `v2-api` gate and no in-repo caller issues PUT. - Close the OpenAPI coverage blind spot that hid two routes: contract discovery was a non-recursive read of the flat `contracts/v2/` directory, so a contract in a subdirectory — or beside its non-v2 siblings, which is where the uploads contracts live — escaped the gate. The sweep is now recursive over the whole contracts tree, and the two upload data-plane routes are named in an explicit allowlist with reasons and staleness guards. - Extract `needsRenderedArtifact` so the "recorded type is authoritative, extension is fallback" rule has one home instead of being duplicated. - Extract `DocCompileUserError` into a leaf module so recognizing it no longer drags `app/api/**` and `next/server` into application modules. - Correct the stale pagination docstring in `contracts/v2/shared.ts` and pin the paged/full-set split in a test so it cannot drift again. * fix(v2-api): absolute imports for the extracted doc-compile error Review follow-up. - Use the `@/lib/...` alias for `doc-compile-error` in the three modules that imported it relatively. The repo requires absolute imports, and having all four consumers share one specifier also removes any chance of two module instances resolving apart and breaking `instanceof`. - Memoize the v2 list-pagination sweep. It re-imported the whole contracts tree once per test and timed out against the default 10s limit under load; it now sweeps once and declares an explicit timeout. Its failure message also still pointed at an enumeration in `v2/shared.ts` that this branch replaced with a pointer to the test itself. * fix(v2-api): correct three inaccurate claims found in verification None of these change behavior; each is a comment or test-config assertion that was not true as written. - The artifact resolver's TSDoc implied the byte cap prevents an oversized artifact being materialized. It does not: the artifact-store fetch is not streaming-bounded, so the bytes are resident before the ceiling rejects them. Say what it actually guarantees. - `v2/shared.ts` pointed at per-contract documentation for the two lists that still filter in memory. Neither contract documents it, so name the two lists and what they do inline instead of pointing at a page that does not exist. - The knowledge update contract said "every field of the body is optional"; `workspaceId` is required. Narrow the claim to mutable fields. - Scope the pagination sweep's extended timeout to the one test that pays for it, so a genuine hang in the other two surfaces in 10s rather than 60s.
…ra, Confluence, and JSM (#6541) * fix(atlassian): share one cached, retrying cloudId resolver across Jira, Confluence, and JSM Every Jira, Confluence, and JSM tool re-resolved its site `cloudId` from `accessible-resources` on each invocation, so a run touching several Atlassian blocks paid a round trip per block and failed outright if any one of them caught a transient fault. A single Atlassian 500 took down a production run this way: the shared retry predicate covers 429/502/503/504 but not 500, so the call was never replayed. Four hand-rolled copies of that lookup now read through one memoized resolver. It caches the promise rather than the value, so concurrent callers join a lookup already in flight and a rejection is evicted instead of pinned for the TTL. Only an exact domain match is retained — a single-site fallback is a property of the calling token, not of the domain, so it answers its own caller without answering the next one. Discovery is an idempotent GET, so it replays transient 5xx. That is scoped here rather than widened into the shared predicate, which also guards non-idempotent writes; it also keeps the failure out of a whole-block replay, which would re-run a write a JSM block had already performed. The budget is tighter than the shared ~31s default — four attempts across ~3.5s — and the request carries a timeout so a wedged fetch cannot strand the callers joined to it. Two defects fall out of the consolidation. `getConfluenceCloudId` never checked the response status, so a 500 parsed as JSON, failed the array check, and surfaced as `No Confluence resources found` — pointing at site permissions rather than the transient fault. `getAssetsWorkspaceId` used a bare fetch with no retry and no cache, leaving the Assets path with two uncached discovery hops. * fix(atlassian): key discovery answers by credential and retry timeouts Review round 1. Three fixes. The cache keyed on the normalized domain alone, so a caller joining a lookup already in flight inherited whichever credential started it — taking that token's authorization failure, or its single-site fallback pointing at a different site. Retaining only exact matches closed that for settled entries but not for the in-flight window, which is where it actually bites. Keys now carry a digest of the access token, so an answer is only ever reused by the credential that earned it. That also removes the reason the cache needed a `retain` channel. The request's own `AbortSignal.timeout` rejects with a `TimeoutError` that has no status and no message the shared predicate matches, so a slow site failed on the first attempt despite the retry budget. It is now explicitly retryable — only `TimeoutError`, since an `AbortError` means a caller cancelled — and the per- request timeout drops to 5s so four attempts stay bounded. Jira bulk read had been pointed at the cached resolver, but the tool's own configured request IS the discovery call and `transformResponse` only runs on a 2xx. It was therefore re-issuing a request whose answer it already held. It now matches against that payload through the shared selector, so the matching logic stays in one place without a second round trip. * fix(jira): treat an empty bulk-read cloudId as missing The consolidation replaced a truthiness check with `??`, so an empty-string `cloudId` counted as supplied and bulk read skipped discovery entirely, building its request URL around an empty id. Back to `||`, matching every sibling tool.
…d routing forms (#6545) * feat(calendly): extend tools with booking, availability, no-shows, and routing forms Adds 12 tools verified against the Calendly OpenAPI spec: get_user, get_event_invitee, create_event_invitee, list_event_type_available_times, list_user_busy_times, list_user_availability_schedules, create_scheduling_link, create/delete_invitee_no_show, list_organization_memberships, list_routing_forms, and list_routing_form_submissions. Also fixes issues found while validating the existing tools: - list_webhooks dropped the scope query param the API requires, so every call with scope unset returned 400 - list_event_types could only send active=true, making inactive event types unlistable - user and organization filters now accept a bare UUID or a full URI consistently across every operation - json array params (eventGuests, events) are normalized whether they arrive as an array or a JSON string * improvement(calendly): type the block/tool alignment test instead of using any * fix(calendly): normalize webhook organization and user identifiers
…il claim (#6546) * fix(auth): let Microsoft sign-in link via Entra's domain-verified email claim Microsoft is excluded from accountLinking.trustedProviders because the email claim is attacker-controllable on /common/ (nOAuth). Entra never emits email_verified for work/school accounts, so Better Auth refused to link a Microsoft identity onto any existing user row, permanently stranding those users on account_not_linked. Derive emailVerified from the xms_edov optional claim, which Entra emits only when the email's domain belongs to the user's tenant and an admin verified it — the one email signal a hostile tenant cannot forge. Microsoft stays untrusted; the guard now passes on its own merits. The mapper returns an empty object when unverified, so it can only ever promote unverified to verified, never downgrade. * chore(auth): drop the unused MICROSOFT_TENANT_ID knob Hosted Sim serves many Entra tenants, so it must stay on the multi-tenant endpoint — pinning is only meaningful for a self-hoster restricting sign-in to their own directory, and nobody is asking for that yet. The xms_edov fix is independent of the tenant setting, so this removes surface without touching behavior. * chore(auth): tighten the Microsoft linking comments
… fetch default (#6547) * fix(tools): bound internal tool calls by the plan deadline, not Bun's fetch default * fix(tools): disarm Bun's fetch idle timer instead of passing a numeric deadline Bun 1.3.14 ignores a positive numeric `timeout` on fetch and honors only the boolean/zero form, so passing the plan deadline through changed nothing and internal tool calls still died at the 300s default. Verified against the pinned runtime: `{ timeout: 1000 }` does not abort a request that takes 3s to answer, and `BUN_CONFIG_HTTP_IDLE_TIMEOUT` has no effect either — both are `main`-only. The caller on this path already arms an AbortController with the plan timeout, so the transport timer is disarmed rather than re-negotiated, leaving one enforcement point instead of two that disagree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(tools): record the measured Bun 1.3.14 timeout behavior Replaces the inferred note with the numbers from a probe against the pinned runtime: no option dies at 300028ms, timeout:false survives 310031ms, and a numeric timeout is ignored. Also records that bun-types@1.3.14 does not declare the option even though the runtime honors it, which is why the interface is declared locally. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t drizzle-orm (#6554) check:sql-date-binding Babel-parsed all 13,941 source files in apps, packages, and scripts. A violation can only come from an `sql` tag resolved through a `drizzle-orm` import, and both the static and dynamic resolvers match the specifier as a string literal, so a file that never names the module cannot produce one. Only ~590 files do. Skipping the parse for the other 92% of bytes takes the audit from ~4.5s to ~0.8s and drops it out of the four slowest audits, taking check:audits from 6.0s to 5.3s wall and 38.0s to 32.9s serial. Output is unchanged.
* fix(files): preserve slashes in folder paths * fix(files): resolve escaped folder lookups
* fix(security): redact opaque workflow snapshot inputs * fix(security): document fail-closed tool redaction * fix(security): redact malformed tool params * fix(security): redact nested credential references * fix(security): isolate opaque tool schemas
* Update tab spawning * update import
* fix(api): wait for knowledge dispatch and encode filenames * fix(knowledge): make document uploads durable * fix(knowledge): serialize document processing attempts * test(knowledge): grant processing attempt claim * fix(knowledge): restore chunk retry metadata * fix(knowledge): reclaim stale processing attempts * fix(knowledge): preserve processing claim ownership * fix(knowledge): restore processing takeover semantics
…e emits (#6592) The internal cancel route mints four outcomes the cancellation service never produces — queue_cancelled, already_cancelled, active_resume_signal_failed and cancellation_not_finalized — but the contract enumerated only the five service reasons. requestJson validates every 2xx against that contract, so a stop that genuinely applied threw on the client. Keep the service enum narrow for the public v2 contract, which delegates wholly to the service and cannot emit the other four, and validate the internal route against a superset. Route the seven success bodies through one contract-typed constructor so drift is a compile error, and delete the dead duplicate cancel contract left behind in contracts/logs.ts.
* fix(tools): bind schema-enrichment reads to executor delegations Two schema-enrichment callers still sent the deprecated legacy internal JWT to routes that moved onto delegation-only auth, so both 401'd and swallowed the failure: - tools/params.ts fetched a child workflow's input fields with an unsubjected buildAuthHeaders(), leaving the Agent block's workflow_executor inputMapping untyped so the model guessed the child's field names. - tools/schema-enrichers.ts fetched KB tag definitions the same way, which dropped the tags/tagFilters parameter from the knowledge tools entirely. Extract the executionId binding rule into executionScopeForTarget so the three enrichment call sites share one definition. * improvement(tools): surface tag-definition read failures at error level Enrichment degrades silently by design, so the log line is the only signal a credential break leaves. Drain the body on the failure path too.
…draft read snapshots (#6594) * fix(logs): snapshot fetch optionality * fix(workflows): restore consistent draft read snapshots
* fix(workflows): redact run and export secrets * fix(workflows): preserve redacted run outputs * fix(workflows): retain safe trace output fallback * fix(workflows): stop deriving outputs from traces
…s it got (#6598) Every PATCH /api/mcp/servers/[id] audit row listed oauthClientId, oauthClientIdProvided and oauthClientSecretProvided — on edits that never touched credentials — while omitting the connectionStatus/lastConnected/ lastError resets the write actually performed. The route always sends `oauthClientId: body.oauthClientId || null` and `*Provided: ... !== undefined`, and null and false both survive a `value !== undefined` filter. The two *Provided flags are control params, not columns at all. Only the writer knows which columns a write touched, so updateMcpServer now returns updatedFields from its updateData and both the internal audit wrapper and the v2 use case record it. This matches workflow-mcp-lifecycle and credentials/orchestration, which already report written columns this way.
…6599) The list use case defaulted to a name sort whose defaults were written for the new v2 contract, so the internal route — whose contract exposes no sort params — could no longer reach the repository's sortOrder ASC, createdAt ASC ordering. Surfaces that render the payload positionally (the @-mention Folders group, the add-resource search results, Copilot's list_file_folders) silently flipped from newest-first to alphabetical, and the Files browser's SSR prefetch hydrated a different order than its own refetch. Push the sort down to the query like the workflow, knowledge, and table folder lists already do, reusing FOLDER_SORTS. Omitting sortBy keeps the position ordering; v2 always sends one from its contract defaults. A name sort now also uses the database collation and the shared createdAt tiebreak instead of a JS comparator over UTF-16 code units.
* fix(tables): tolerate row deletion during run cancellation * fix(tables): unwrap row deletion errors
* fix(files): restore CSV preview cancellation * fix
finalizeExecutionAttachment reported a completedFileId. That marker is what
routes a replayed completion into loadCompletedUploadPurpose, which handled
only workspace_file and threw a bare Error otherwise -- unclassified, so the
route rendered a generic 500. Its structurally identical twin,
finalizeMothershipAttachment, correctly reports nothing.
Both are metadata-backed and idempotent by storage key, exactly as the
finalizeUploadPurpose TSDoc already states, so neither needs the marker: their
replays are correct through the finalizer itself. Drop it from the execution
finalizer so the two twins agree.
loadCompletedUploadPurpose becomes an exhaustive switch, matching the sibling
finalizeUploadPurpose switch, so adding a purpose is a compile error until its
replay behavior is decided rather than a runtime 500. The residual arm throws a
classified UploadSessionError('internal') instead of a bare Error.
markUploadSessionCompleted no longer clears a marker it was not given. A
finalizer that records one inside its own registration transaction --
markUploadSessionFileRegistered does this for workspace_file -- would otherwise
have it overwritten with null, and both the abort guard and the expiry sweep
key on it: cleanupExpiredUploadSessions only treats a finalizing session as
disposable when completedFileId is null. This is a no-op for every current
path, since markUploadSessionCompleted moves the session to completed, which is
neither abortable nor a cleanup candidate.
Latent only. No shipped client replays a completion: the sole producer,
uploadWorkflowAttachments, mints a fresh session per file and never retries,
requestJson does not retry, and a concurrent double-submit is already a clean
409 from claimSession.
Tests pin the invariant rather than the symptom: a Record over the purpose union
is a compile-time gate on which route each purpose replays through, and the
cases assert that idempotent purposes report no marker and reject cleanly if
they ever reach the loader.
…6603) * test(table): pin the executor auth pairing on the table read route fetchTableSchema reaches GET /api/table/[tableId] with a legacy type:'internal' token, which only works while that route authenticates through checkSessionOrInternalAuth. Its sibling table routes already moved to the delegation policy, which rejects that token outright, so migrating this one without moving the caller in the same change would break every table tool on an Agent block. Assert the route still authenticates through the legacy path so that migration fails here first, and record on the caller why it is deliberately not on buildExecutorDelegationHeaders yet. * test(table): assert the Bearer header reaches the legacy verifier Address review: the pairing test sent no Authorization header and its name claimed to verify token acceptance, which is pinned separately in lib/auth/internal.test.ts. Send a representative header, assert it reaches checkSessionOrInternalAuth unmodified, and scope the name and docs to what this guard actually covers — the route's choice of verifier.
Transcribed Agiloft REST docs committed by accident in #6562 alongside the alrest repointing. Nothing imports or links it, it sits in a dot-directory at the repo root rather than anywhere docs live, and the integration it informed has shipped. Contents are the vendor's own public examples — placeholder credentials and localhost hosts only, so nothing sensitive was exposed while it was public.
* refactor(audit): derive updatedFields through one shared helper Six copies of Object.keys(updateData).filter(k => k !== 'updatedAt') across four files decided, independently, which columns an audit row reports. The exclusion set is an audit convention, not a local detail, so it moves to @sim/audit as auditUpdatedFields and the exclusion becomes a single edit. The admin organizations route evaluated the expression twice in one handler and filed it under the metadata key `fields` while every other site uses `updatedFields`, so any consumer filtering on updatedFields silently missed org updates. It now computes once and uses the shared key; nothing reads metadata.fields. auditMock carries the real implementation rather than a stub, since callers under test derive their audit metadata through it. The two suites that hand-roll an @sim/audit factory source it from there. * test(audit): pin the testing mock's copy of auditUpdatedFields @sim/audit devDepends on @sim/testing, so the mock cannot import the real helper without closing a package cycle. Assert parity from the audit side instead, where the dependency already runs the safe direction, so a change to the exclusion convention cannot leave mocked callers validating behavior the deployed helper no longer has.
* fix(mcp): audit an upsert that rewrites or revives a server Registering a URL that already exists takes the upsert branch and rewrites the live row — name, transport, headers, timeout, enabled, auth type, the connection reset, and the URL's query string, since the server id hashes only origin and pathname. That branch recorded no audit row at all: the ADDED audit was gated on `!result.updated`. main recorded ADDED for these (wrong action, but a row existed), so this restores coverage and fixes the action. Reachable from the settings POST /api/mcp/servers and from Copilot's manage_mcp_tool `add`, neither of which passes existingServerBehavior. The v2 POST passes 'reject' so it only reaches the upsert on a revival. A rewrite is now MCP_SERVER_UPDATED carrying updatedFields; a revival of a soft-deleted row stays MCP_SERVER_ADDED. updateValues is typed Partial<$inferInsert> so Object.keys is column-safe. Analytics gating is unchanged: mcp_server_connected still fires only for a genuine insert. * fix(mcp): redact audit URLs and drop unwritten columns from updatedFields Two review findings on the new upsert audit. The upsert assigns every column unconditionally, so `description` is present on updateValues but undefined when the registration omits it. Drizzle skips undefined in .set(), so deriving keys without checking values made the audit claim a column the write never touched. Filter by value; null stays, since clearing a value is a write. MCP URLs carry tokens in their query string — that is why a silent rewrite of one matters — and audit rows are readable by org admins who need no workspace MCP access. Newly auditing rewrites would persist those tokens verbatim, so every MCP audit row now records the URL through sanitizeUrlForLog, which strips query and fragment. Applied to the add, update and delete rows alike: redacting only the new path would leave the same credential in the row a first registration already writes. A null url stays null rather than becoming an empty string.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
|
Too many files changed for review (2352 files, 100 file limit). |
PR SummaryHigh Risk Overview Desktop. Adds a native macOS Help-search addon (trusted docs.sim.ai only), microphone entitlements for voice input, and polished DMG packaging. Renames prerelease streams from Architecture & agent guidance. Codifies the Application Operation Boundary: protected reads/writes go through one authorized use case shared by internal, v2, Copilot, and tools. Adds the APIs, workflows, security. Ships cleaned-up v2 endpoints and docs, workflow card/progress/DSL/retry UI, and many authz/compatibility fixes (credential boundaries, secret redaction, cross-tenant concealment, legacy endpoint restore). Also extends Salesforce, incident.io, Calendly, and Agiloft integrations. Reviewed by Cursor Bugbot for commit b5d9e93. Configure here. |
getBlobPresignedUploadUrl signed its SAS with BlobSASPermissions.parse('w').
Per Azure's service-SAS reference, `w` is "create or write content" and permits
overwriting an existing blob; `c` is "write a new blob" and does not. main
signed `c` before this signer moved out of core/storage-service.ts, so Azure
deployments lost create-only enforcement in the move.
The `If-None-Match: '*'` the signer returns cannot carry the guarantee on its
own: an Azure service-SAS string-to-sign covers the resource, times,
permissions and the five rsc* response-header overrides, never request headers,
so a client is free to drop it. The header is signed on the other two providers
-- inside the PutObjectCommand on S3, and as x-goog-if-generation-match in
signed extensionHeaders on GCS -- which is why only Azure regressed.
Without this, a signed upload URL stayed a plain overwrite grant on the final
key for its full hour. A caller could replace the object after complete had
already verified size and content type, written the workspace file row and
metered storage from that verified HEAD, leaving durable metadata and billing
describing content that no longer exists.
The multipart block-staging signer keeps `w`: block staging is overwrite-shaped
and matches main.
The existing test asserted parse('w'), so it locked the defect in; it now
asserts create-only, and its name states the guarantee so a future flip reads
as deleting a security property rather than adjusting a value.
Uh oh!
There was an error while loading. Please reload this page.