v0.8.1: grok 4.6, v2 endpoints extension, workflow UI improvements - #6646
v0.8.1: grok 4.6, v2 endpoints extension, workflow UI improvements#6646waleedlatif1 wants to merge 35 commits into
Conversation
…s arguments (#6621) The workspace file preview adapter runs while the tool-call frame is still on the wire, so the execution context it gets is turn-scoped and carries no toolCallId — the file delegation requires one, so resolving a path target threw on every call. The SSE handler swallows that throw and abandons the rest of the event, so the frame never registered its arguments and the call was later dispatched with an empty payload, failing schema validation. - bind the frame's own tool call id before entering the file use cases - resolve the preview target best effort, matching the preview base load - stop a preview failure from dropping the tool-call frame in the stream loop - drop the synthetic toolCallId the stream fixtures put on a turn context
… envelope holes (#6620) * fix(v2): give every collection one pagination contract and close four envelope holes A fractional `limit` reached Postgres as `LIMIT 2.5` and answered 500 on both `GET /workflows` and `GET /audit-logs`: each list re-declared the param inline, and these two copies lost their `.int()`. The same divergence left `limit` validated five different ways and five collections emitting `nextCursor` while accepting no `limit` at all, or accepting one and silently discarding it. Adds `v2PaginationFields()` in `contracts/v2/shared.ts` — a bounded integer `limit` and an opaque `cursor` — and adopts it across all 17 paged lists, so the family cannot drift again. `/files`, `/logs` and `/tables` keep the truncate-and- clamp leniency they published, now as an explicit named mode rather than three hand-rolled copies. Gives `/skills`, `/custom-tools`, `/secrets`, `/credentials` and `/knowledge` real pagination using the existing cursor codecs: a keyset for the four whose page comes from one ordered SQL read, and the offset cursor for `/skills`, whose merge of the static builtin registry into DB rows cannot be expressed as a SQL keyset. Each keyset sort now ends in a unique `id`; knowledge tie-broke on `createdAt`, which cannot separate rows sharing a millisecond. Two correctness fixes pagination forced: the secrets visibility filter moved from a post-query JS pass into SQL, because trimming rows after the page is cut returns fewer than `limit` while `nextCursor` claims more; and the skills list stopped selecting the 50k-char `content` column only to discard it. Also restores the canonical error envelope where it had holes: a malformed JSON body returned a bare `{"error":string}` because the envelope was a per-route opt-in only 8 of 77 routes remembered, and an unknown `/api/v2` path returned an HTML 404. Both are now defaults — `V2_PARSE_DEFAULTS` on the builders and the two raw routes, and a catch-all whose body is byte-identical to the rollout gate's so an unknown path stays indistinguishable from an ungated one. Consolidates the keyset paging block (`resumeKeyset`/`keysetPage` in `list-query.ts`) that had been open-coded in six modules, and folds the bespoke `InvalidWorkflowListCursorError` into the `OrchestrationError` every other list already used. Prevention: the contract sweep in `list-pagination.test.ts` now also asserts that every paged list rejects a fractional `limit`, that every list query is `.strict()`, and that the three clamping lists still truncate. The fractional- limit assertion is what caught `/audit-logs`. Documented in `.agents/skills/v2-api-conventions/SKILL.md`. 405 responses still carry no `Allow` header — Next.js generates those before any handler runs. Recorded as a known gap. * fix(v2): bind the offset cursor to the query state it counts positions in An offset names a position in one exact sequence. `GET /skills` accepted a bare `{offset}` cursor and applied it to whatever sequence the next request asked for, so following `nextCursor` with a different `search`, `sortBy` or `sortOrder` silently skipped rows, repeated them, or landed past the end and returned an empty page while the cursor implied more. Fixed in the codec rather than the route so the sibling could not keep the gap: `decodeOffsetCursor` now takes a scope stamp and rejects a cursor minted under a different one, which is what `decodeSortedCursor` has always done for keysets. `offsetCursorScope()` builds the stamp from every param that filters or orders the sequence; `limit` is excluded because it selects how much of the sequence to return, not what the sequence is, so paging with a different page size still works. `GET /knowledge/{id}/documents` had the identical latent gap and gets the same treatment — the compiler surfaced it as soon as the signature changed.
Verified every field against xAI's live API with the staging hosted key: context_length 500000, $2.00/$0.50/$6.00 per 1M, temperature capped at 2, tool calling and max_completion_tokens both accepted. Also feature the latest blog post.
* feat(xai): wire reasoning effort through the Grok adapter The catalog never declared reasoningEffort for xAI and the adapter never sent reasoning_effort, so the flag was dead for every Grok model. Values are per-model and verified against the live API rather than the docs, which are wrong in three places: grok-4.5 does accept xhigh, grok-4.3 supports the parameter at all (undocumented) including none, and grok-4.20-0309-reasoning rejects it outright despite being a reasoning model. Also corrects grok-4.5's missing cachedInput and drops an inline comment the new provider TSDoc now covers. * test(xai): type the provider test helper instead of casting to any * fix(agent): correct reasoning-effort copy that still claimed GPT-5 only
… retried (#6625) * fix(v2): tell a caller when to come back on every failure meant to be retried Three related gaps in retry signalling, found auditing the v2 surface against RFC 9110/6585 and against how Stripe, GitHub and Google's AIPs handle the same problems. **No 503 carried `Retry-After`.** Every one of them — the three route builders' `unhandledErrorResponse`, the execute and resume routes, and `serviceFailureResponse` — funnels through `v2Error`, so the default lands there, keyed on the response *status*: `Retry-After` is defined against the status, and the status is the only half of the code/status pair a client sees. A caller that supplies its own value still wins. RFC 9110 §15.6.4 makes this a `MAY` rather than a `SHOULD`, so it is a deliberate improvement, not a conformance fix: without it a client's only defensible policy on a 503 is an immediate retry, and Sim raises 503 exactly when a dependency is too degraded to absorb one. **A 429 that already knew its wait threw it away.** The admission descriptors declare `retryAfterSeconds` per denial, but mapping a descriptor onto a preprocess error copied only `statusCode`, `code` and `retryable`. A concurrency denial therefore reached the client as a bare 429 with no `Retry-After` despite the policy layer having named the wait five seconds earlier. The value now travels `descriptor.retryAfterSeconds` → `PreprocessExecutionError.retryAfterMs` → `ExecuteWorkflowServiceFailure.retryAfterMs` → `serviceFailureResponse`, so the transport reads a number the policy owns instead of re-guessing one. The 503 default is now only the floor for paths with no policy signal. **One failure must not advise a retry at all.** `ASYNC_ENQUEUE_AMBIGUOUS` is a 503 whose enqueue may have succeeded — it deliberately retains its execution-ID claim because a job may already exist. Telling that caller to come back in five seconds invites a client with no `X-Run-Id` to start, and bill, a second run of the same workflow. It opts out via `omitRetryAfter` and returns the run id so the caller reconciles instead. `ADMISSION_RETRY_AFTER_SECONDS` is reused rather than restated, so the execute route's capacity 429 and every other surface's 503 cannot drift apart. Also records the audit in `.agents/skills/v2-api-conventions/SKILL.md`: the retry rule, the cursor-tampering invariants, and reasoned rejections of RFC 9457 problem+json, the `RateLimit-*` draft fields, renaming `X-RateLimit-*` under RFC 6648, 422-for-semantic-validation, `Location` on 201, ETag/`If-Match`, and `merge-patch+json` — each with the spec text and the industry evidence, so they are not re-litigated. `Deprecation`/`Sunset` on v1 is left open pending a retirement date, which is a product decision. * docs(v2): name the one 503 that omits Retry-After in the shared contract The shared ServiceUnavailable description claimed every 503 carries the header, which the ASYNC_ENQUEUE_AMBIGUOUS response deliberately does not. It now says the header is normally present and names that exception, so the published contract matches the runtime behaviour for all 128 operations.
…6623) * fix(v2): serve HEAD, advertise PATCH, and document the reachable 403 Three HTTP-semantics defects on the v2 surface, all found by probing the published contract rather than the happy path. **HEAD answered 500 on every v2 endpoint.** Next implements a missing `HEAD` export by aliasing it onto `GET` and dropping the body when it sends, so a route's `GET` legitimately runs with `request.method === 'HEAD'`. The builders' method guard compared that against the contract's declared method and threw, so `HEAD /api/v2/workflows` and every sibling replied 500 — which is what health checkers, uptime monitors, link checkers, and some CDNs send, all of them reading the API as hard-down. RFC 9110 §9.3.2 makes HEAD identical to GET but for the body, which is exactly what running the GET path produces. Fixed once in `methodMatchesContract`, shared by all five route builders; every other mismatch stays a hard error so a handler exported under the wrong verb still fails loudly. **CORS advertised `GET,POST,OPTIONS,PUT,DELETE`** while the v2 spec has 17 `PATCH` operations, so a browser preflight for any of them was rejected. It also advertised `PUT`, which two operations use — the shape of a hand-maintained list outgrown by its surface. The list stays hand-written because middleware cannot import the contract tree without pulling Zod into the edge bundle, but it is now pinned by a test that sweeps the real contracts and fails on any method it omits. **Six operations omitted a 403 their siblings documented** — three knowledge reads and three file-upload operations. Traced from the code rather than the spec: `requirePermission` throws `NoWorkspaceAccessError` for no access at all (concealed as 404) but `InsufficientWorkspacePermissionsError` for access below `minimumRole` (a real 403), and `PersonalApiKeysDisabledError` reaches every operation a personal API key can call. So 403 was reachable on all six and the omission was an accident of hand-assembled error lists, not a policy. They now use the shared `RESOURCE_ERRORS` / `RESOURCE_CONFLICT_ERRORS` sets, and two operations spelling those same sets by hand were normalized onto them. All 128 documented operations now declare 403. The rules for HEAD, for the 403/404 split, and for using the shared error sets are recorded in `.agents/skills/v2-api-conventions/SKILL.md`. * test(proxy): update the CORS policy assertion to the served method list `proxy.test.ts` pinned the previous hand-written method string, so widening `resolveApiCorsPolicy` to advertise PATCH and HEAD left it asserting a list the middleware no longer returns. The literal is kept rather than imported from `proxy.ts` so the test still pins the exact wire value independently of the implementation. * refactor(v2): retire the error sets that could omit Forbidden The three knowledge reads and three upload operations lost their `403` by assembling `[...VALIDATED_ERRORS, ...]` by hand, and `VALIDATED_ERRORS` / `STANDARD_ERRORS` were the only exported sets that omit `Forbidden`. Migrating the last consumers to the shared `RESOURCE_*` sets left both unreferenced, so deleting them turns the fix from a one-time cleanup into an invariant: there is no longer a building block from which a workspace-scoped operation can assemble an error list without `Forbidden`. Regenerating the specs produces no diff, so the migration is output-neutral. Also folds `method-match.test.ts` into `definition.test.ts` to match the repo's `feature.ts` -> `feature.test.ts` convention, types `contractMethod` as `HttpMethod` so a contract declaring `HEAD` is unrepresentable, and drops the duplicated Next-aliasing rationale so `methodMatchesContract`'s TSDoc is its single home. * fix(cors): expose the API response headers a browser client needs Without `Access-Control-Expose-Headers` a browser can read only the six CORS-safelisted response headers, so the rate-limit budget, the `Retry-After` a 429 or 503 asks the caller to observe, and the request/run correlation ids were all on the wire but invisible to `fetch()`. Server-to-server callers were unaffected, which is why it went unnoticed. Exposed on the default `/api` policy only. The per-route `CORS_RULES` entries are wildcard-origin public endpoints and opt in individually if they ever need it, so this does not widen what an anonymous cross-origin caller can read from them.
… tip (#6632) The connection knob is a recolour of one stretch of the card outline, so its path has to be the outline's own path. Two things pulled it off: Its span was cut at the exact point the bulge falls under the visibility threshold, which is not one of the points the silhouette sampled — so the knob sat on a grid of its own. And a span clamped its first and last control points to the bare perimeter tangent, where the silhouette derives every control point from the samples either side of it, so those two segments bowed differently from the curve they were painted over. The knob was left still flat where the silhouette had already begun its descent, and the uncovered sliver of dark stroke read as a small spur poking off the shoulder. It is clearest on the Error output's outer shoulder, against the card's bottom-right corner. Measure the span against the interval the silhouette resamples the bulge over rather than in whole pixels, and sample a step wide on each side before trimming back, so every emitted segment has the neighbours the silhouette had. The knob's commands now come out identical to the silhouette's, which the tests pin — including for merged intervals and odd tab lengths, where measuring in whole pixels would still have landed half a step off. The painted footprint is unchanged.
A block's tile disagreed with the card it named. The canvas brands only third-party integrations and gives everything first-party its role accent, but the command palette, connection lists, tag menus and output pickers all painted straight from the catalog `bgColor` — so Webhook Trigger showed green in the palette and blue on the canvas it was about to be dropped onto, and the five roleless first-party triggers showed catalog blues where the canvas shows neutral. Read the canvas rule from one place (`hasBlockAccent`) and render it through one component (`BlockTile`), then point every surface that lists a block at them: canvas, editor header, preview, toolbar, palette, connection picker, terminal, logs trace rows, connection lists, tag menus, output pickers and the tables workflow sidebar. Folds in the duplication the split had grown: three copies of `TagIcon`, five hand-rolled tile divs, the toolbar's second encoding of the accent rule, a third icon-contrast helper on its own brightness threshold, and the dead `showColoredIcon` prop every caller passed. Tiles now share the chip radius, and the tile forces its own icon colour so popover and command rows painting `[&_svg]:text-*` can no longer wash out a pale brand tile. Large detail headers (preview panel, trace-view detail) keep their own treatment and are left for a follow-up.
* fix(copilot): surface document render failures * Address PR review feedback (#6629) - validate render errors against workspace-file provenance before returning details\n- cover blocked provenance with a regression test * Address PR review feedback (#6629) - mark every non-throwing render failure as a failed dynamic read\n- cover all soft render failure paths with producer-level tests\n\nNote: pre-existing type-check failures in HEIC and provider files are not addressed by this PR.
Handing back the `nextCursor` from any timestamp-sorted v2 list and passing it straight in returned 500. The keyset compares millisecond-truncated timestamps on both sides, and the bound cursor value went out as a bare placeholder — which Postgres types as `unknown`. `date_trunc` is overloaded across `timestamp`, `timestamptz`, and `interval`, so `date_trunc(unknown, unknown)` matched no single candidate and the statement failed outright. The value was already validated; it just carried no type. Cast it to the column's own SQL type inside `timestampKey`, so all twelve call sites across six modules inherit the fix. Derived from the column rather than hardcoded, which keeps a `timestamptz` column's offset honoured too. The millisecond truncation is unchanged — it is what stops the page's own last row being re-admitted.
…t's own box (#6638) * improvement(workflow): smooth the running hatch's slanted edges The marks read as stepped rather than slanted. A repeating gradient is sampled once per pixel with no coverage term, so a hard colour stop on an edge 15° off vertical can only land wholly on one side or the other — there is no partial value to soften the transition, and the staircase is the whole edge on a mark this thin. Ramp each edge over 0.75px, roughly a device pixel, instead of switching colour at a single offset. That hands the rasterizer the intermediate values antialiasing would have produced: measured deviation of the edge from its own straight line falls from 0.28 device px — pure quantization — to 0.05. The ramps are centred on the offsets the hard stops used, so the 50%-coverage line does not move: same 75° lean, same 24/2 rhythm, same 26px scroll period. * improvement(workflow): sit the running hatch in the slot's own box The hatch was inset 4px into a 24px row, so it stood 16px tall inside a swell whose slots are 24px — it read as a shorter bar floating inside the row rather than as the slots themselves filling, and its right end stopped short of where a hovered slot's fill ends. Span the row instead. The row already sits inside the container's 2px/3.2px inset, so occupying it outright puts the hatch in exactly the box a slot's hover fill occupies: same height, same padding in from the swell on every side. The end taper has to move with it, since its two numbers were read off the slot's diagonal at the old overlay's top and bottom (y=4 and y=20). Continuing that same edge — slope 20/24 — across the full row gives 20px in at the top and flush at the bottom, so the hatch still ends on the slot's own diagonal. * fix(workflow): feather both hatch edges, not just one The trailing ramp straddled the period boundary. Anchored at 0, the mark's leaving edge ramped 24.735 → 25.485, but a repeating gradient truncates at its own wrap, so it was cut at 25.11: half the feather, and its 50%-coverage line pulled 0.19px inward. That edge stayed sharper than the other and the gap rendered 1.75px instead of 1.93px. Run the period centre-of-mark to centre-of-mark instead, so both ramps sit strictly inside it. The stop list still tiles backwards from its first stop, so the marks land where anchoring at 0 put them — measured pitch is unchanged at 26px and both edges now carry the full 0.75px.
* feat(windchill): add document integration * fix(windchill): align tool contracts and docs * fix(windchill): use official integration icon * fix(windchill): correct response and paging semantics * fix(windchill): align execution and API contracts * refactor(windchill): inline route authentication * fix(windchill): correct OData query encoding, content download, and cleared-field handling Validated the integration end to end against PTC Windchill REST Services 2.7 documentation and fixed every divergence found. Protocol correctness: - Encode OData query spaces as %20 rather than the form-encoded `+` that URLSearchParams emits. Every multi-token $filter and $orderby reached Windchill as a literal `+` and could not match. - Download content through the documented typed navigation `<content>/PTC.ApplicationData/Content/URL`, which returns a signed vault URL, instead of a `$value` segment that WRS does not implement. The resolved URL is pinned to the configured HTTPS origin. - Terminate every Stage 2 CacheDescriptor_array entry with `;` to match the documented grammar. - Raise the $top bound to Windchill's documented 2000 maximum, keeping 200 as the default page size. Cleared-field handling: - The executor merges raw block inputs before the block's param transform, so omitting a key could not clear it. A cleared numeric or boolean field reached the URL builder as '' and threw, and cleared optional strings failed contract validation. Coercions now emit an explicit undefined, and the internal-route body strips blanks centrally. Robustness and contracts: - Bound the document-structure walk to the depth actually requested. - Loosen response schemas that re-applied request-side bounds to provider-returned values, which turned committed mutations into opaque parse failures. - Return contract-shaped bodies for oversized, malformed, and unhandled request failures. - Normalize downloaded content types and drop charset parameters. Presentation and docs: - Square the icon to a centred tile on white. - Replace WT.Document and PATCH-compatible jargon with plain language. - Fix canvas sentence noun stutters on the bulk operations. - Correct the revision skill's unverified working-copy claim to read the OID back rather than assume it, and add retirement and stale-checkout skills. - Add a manual intro section to the integration docs page. * fix(windchill): align tool copy with the docs page and rebase the route baseline Tool descriptions feed both the integration catalog and the generated docs page, so the plain-language pass had to reach them too: drop WT.Document and PATCH-compatible from the operation copy, and correct the $top bound the descriptions still advertised as 200. Correct the docs intro's attachment wording, gloss OData on first use, and attribute the bulk-atomicity claim to PTC's documented behavior. Raise the API route-count baseline, which staging advanced while this branch was behind. * feat(windchill): add update common properties Name, Number, and Organization are rejected by the PATCH-based update operation, and the rejection message told users to reach for Windchill's UpdateCommonProperties action that the integration did not expose. Add it. PTC documents UpdateCommonProperties as a bound DocMgmt action taking an Updates wrapper, available when hasCommonProperties is set on the Documents entity, and refused while the document is checked out. The subblock and param descriptions carry that constraint, and the rejection message now names the operation that does the job. * test(windchill): assert block and tool params stay aligned for every operation Validating the new operation surfaced that nothing enforced the block-to-tool alignment the review process had been checking by hand. Assert it for all 27 operations instead: every required tool param has a required, non-advanced input under that operation's condition, and no operation shows an input its tool cannot accept. Both fail on a deliberately broken condition or a dropped required flag. --------- Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local> Co-authored-by: Waleed Latif <walif6@gmail.com>
) * fix(workflow): stop subflows resizing themselves after every load A container sized itself from its children, and when a child had not yet reported a height it used `estimateBlockDimensions` in its place — a guess of `ceil(subBlockCount / 2)` rows, which read a 39-field Gmail card as 276px tall against the 112px it draws. The container painted that number, the real height arrived a frame later, and it visibly resized between the two. Nothing is persisted, so it happened on every refresh. A card's height depends on what it actually renders — which rows survive its conditions, whether it draws a summary sentence, and for a reactive field even a credential it has to fetch — so the card is the only thing that can know it. Size only from heights the children have themselves reported, and hold the container at its current size until they have. `getBlockDimensions` keeps the estimate for the callers that only need a rough box (clamping a drag, placing a paste) and is now that same lookup plus the fallback. Also stop `calculateContainerDimensions` counting the container's chrome twice. Child coordinates are relative to the container's own origin and are already held clear of the header by `clampPositionToContainer`, so a child's far edge is the distance to cover and only the trailing padding is owed on top. Adding the header and leading padding again left every container 66px taller and 16px wider than its contents. * fix(workflow): gate container sizing on this session's reported layout Two holes in the measurement gate, both from reading the wrong field. `height` is a persisted column and `data.width` / `data.height` persist a container's last size, so a block that has not reported yet can still carry last session's numbers — reachable through paste, import, and checkpoint restore. The gate treated those as reported and sized from them. Nested containers had it worse: an inner container with an unreported descendant handed back its 500x300 default as though it were measured, so the outer container sized to that and resized again once the descendant filled in — the same two-step this change exists to remove. `layout` is in-memory only and written by exactly the two places that know: a card through `updateBlockLayoutMetrics`, a container through `updateNodeDimensions`. Reading it means "reported during this session" and nothing else, and an inner container that is still waiting reports null, so the outer one waits with it. `getBlockDimensions` keeps the persisted height and the estimate as fallbacks — its callers only need a rough box, where a stale height still beats a guess. * Revert "fix(workflow): gate container sizing on this session's reported layout" This reverts commit 3cce724. * fix(workflow): size containers from a state-aware child estimate The gate in the reverted commit held a container at its current size until its children reported. That is worse than it sounds: the size it holds is the persisted default of 300, the child needs 335, and so the child hung outside the container until something forced a resize. Estimate accurately instead of waiting. `getBlockMetrics` derives a card's height from the block's own state — the sub-blocks its values leave visible, the summary sentence, the error row — through the same `calculateWorkflowBlockDimensions` the card calls, and lands on the height the card goes on to render: 112px for the Gmail card the type-only estimate put at 276px. The pass before the cards report and the pass after now produce the same container, so there is nothing to gate and nothing to correct. This also fixes the guess everywhere else it was painted rather than only in the container path — `estimateBlockDimensions` fed React Flow's node height for unmeasured blocks, so selection bounds were 276px around a 112px card. * improvement(workflow): even out the gutter inside a container Left, top and bottom were 16 and the bottom read tighter than either, because the 50px header sits above the top gap and gives that edge visual weight the other two do not have. Taking them to 24 leaves the three gutter-only edges matching and the bottom no longer pinched. Right stays 80. The container's output handle sits on that edge, so a child needs clearance there it does not need anywhere else — chrome rather than gutter, now said so in the type. Only reachable as a single constant each because the paddings mean what they say: each is the gap between a child's edge and the container's, counted once. While the sizing math added the header and leading padding a second time, the effective bottom gap was spread across three constants and tuning it meant reasoning about all of them. * improvement(workflow): give a container one source for its own gutter The four paddings and the header height existed twice: as `CONTAINER_DIMENSIONS`, which sizes a container and clamps its children, and again as Tailwind literals in `subflow-node-view`, which draws the header and the content box. Nothing kept them in step and they had already drifted — the view rendering a 40px header against a constant claiming 50, so children were clamped 10px below where the header actually ends. The view now renders from the constants, and the constant follows the DOM at 40. Match the bottom gutter to the right at 80. The two edges that carry chrome are now the two that are wider: the container's output handle sits on the right, and the resize grip in the bottom-right corner spans 40px in from both, so a child at the 24px gutter width could sit underneath it. Left and top are only gutter and stay at 24. * test(workflow-renderer): assert the subflow header's height, not its class The header renders from `CONTAINER_DIMENSIONS.HEADER_HEIGHT` now, so the class it used to carry is gone. Assert the rendered height against the same constant the layout math measures against — the two drifting apart is what this whole change is about, and a utility-class assertion cannot catch that. Also set `IS_REACT_ACT_ENVIRONMENT`, which these tests have always needed. React only treats `act` as supported when it can see the flag, so every render logged "The current testing environment is not configured to support act(...)" — around forty lines of it per run, burying the actual failure output. * fix(workflow-renderer): declare the act-environment global `vitest.setup.ts` is inside the package's tsconfig, so assigning an undeclared property on `globalThis` failed type-check (TS7017) even though the tests ran. * fix(workflow): size a container from one snapshot of the store `calculateLoopDimensions` took child positions from the live store but child dimensions from the hook's render snapshot, so it was reading two ages of the same data. `resizeLoopNodes` walks deepest-first: an inner container resized earlier in the pass was already updated in the live snapshot and still stale in the closed-over one, so its parent sized against the old inner box and only caught up on a later render — a nested container visibly resizing twice, which is the symptom this branch set out to remove. Take both from the snapshot the function already reads. * fix(workflow): size an unmeasured note as a note Routing every non-container block through `getBlockMetrics` sent notes through the workflow-card estimate, which counts sub-block rows and an error row a note does not have. A note that had not reported a height yet got a card's box, so a container holding one sized itself around the wrong shape. Give a note its own branch, as the estimate it replaced did: measured height when there is one, and the height an empty note paints when there is not.
* fix(v2): close four validation holes in the logs and billing surfaces
Each of these answered a caller-supplied value with a 500 or a silently
wrong result instead of a 400.
- `GET /api/v2/logs` accepted any string as `startDate`/`endDate`. The
route constructs a `Date` from it, so `?startDate=abc` reached the
driver's timestamp mapper as an `Invalid Date` and 500'd. Both bounds
now carry `.datetime()`, matching the sibling run list so one timestamp
works on both collections. This narrows the accepted set: a date without
a time and an offset-bearing timestamp are now rejected, and the field
descriptions say "UTC ISO 8601" rather than overpromising "ISO 8601".
- `v2BillingStatusQuerySchema` was the only non-strict query schema in its
family, so a mis-cased `workspaceID` was stripped and the caller got
account-scope billing in place of the workspace scope it asked for — a
wrong answer about money, served as a 200.
- An unresolvable `cursor` on `/api/v2/billing/logs` applied no cursor
condition and restarted the sequence at page 1 while still reporting
`hasMore`, so a pager holding a cursor across a deploy loops over the
first page and counts the same credits on every lap. It is now a 400.
The message does not reuse `INVALID_CURSOR_MESSAGE`, which names
`sortBy`/`sortOrder` params this collection does not accept.
- The logs `status` field disagrees with the run resources for the same
run: the run projection overlays `paused` from `paused_executions`,
so an ordinary human-in-the-loop pause reads `paused` there and
`pending` here. Reconciling would mean joining `paused_executions` in
this read and silently moving live runs between two buckets of a
shipped field, so the divergence is documented on the contract instead.
* feat(v2): expose the MCP tool plane and page the MCP server list
Registering an MCP server through v2 dead-ended: nothing on the public
surface ever ran tool discovery, so connectionStatus, toolCount, lastError,
and lastToolsRefresh stayed at their registration defaults and there was no
way to read a server's tools without opening the UI.
Adds GET /api/v2/mcp-servers/{id}/tools over a thin use case composed from
the existing mcp_servers.tools.discover operation, resolveServerContext, and
mcpService.discoverServerTools. It is personal-API-key-only — discovery
resolves the acting user's own OAuth credentials, which a workspace key
cannot supply — and the contract says so rather than letting callers meet an
unexplained 403. Discovery failures are classified instead of collapsing
into a 500: an unreachable or cooling-down server is a retryable 503, a
stale OAuth grant is a 401.
Also pages GET /api/v2/mcp-servers. It was the one unbounded list on the v2
surface, classified full-set on a bounded-by-construction rationale that
only holds for folder lists; nothing caps how many servers a workspace
registers.
* feat(v2/tables): strict row bodies, a filtered row count, and round-trippable required columns
Three tables gaps from the v2 capability evaluation.
Strictness. Every v2 tables request body is now `.strict()`. The row family
was the whole hole: `POST /query` sent v1's `filter` key answered 200 with a
fully unfiltered page, because Zod strips unknown keys unless told not to. The
same laxity covered the row create/update/delete/upsert/find bodies, the
run and cancel-runs bodies, the enrichment body, and — outside the row family
but the same class — the column delete, view create/update, and export bodies.
A contract sweep now walks every body-bearing tables contract and fails if one
of them stops rejecting an unrecognized key.
Filtered row count. `POST /api/v2/tables/{tableId}/query/count` answers the
question v1's `includeTotal`/`totalCount` answered and the `{data, nextCursor}`
envelope has nowhere to put: how many rows a predicate matches. It binds the
existing `queryTableRows` use case with `includeTotal: true, limit: 1` — no new
domain logic and the same `tables.rows.query` read policy. The use case types
`totalCount` as nullable because paged callers can decline it; this route always
asks for it, so a null is treated as a broken invariant rather than presented as
a fabricated zero.
Required columns. `required` is accepted on create-table, add-column, and
update-column, matching v1. v2 emitted the flag on every read while stripping it
from every write, so a column could not round-trip. Enforcement was already
complete: turning it on over rows with null, missing, or empty cells is rejected
by the domain.
* test(skills): pin the workspace-API-key split as structural, not accidental
A workspace API key can create a skill it can then never update or delete,
which no sibling resource does — so the asymmetry reads like an oversight
worth widening. It is not. Skill edits are authorized by the per-skill
editor row belonging to the acting user, which is why update/upsert/delete
declare a 'read' floor rather than 'write': workspace role is not the
authority. A workspace key carries no user subject, so allowing one replaces
a 403 with an unclassified PrincipalSubjectUserRequiredError that the v2
surface renders as a caller-reachable 500.
Records the reason on the registry and pins it, so the next reader finds the
argument instead of flipping the flag.
* feat(v2): read deployment state, and undo a file delete
Two v2 reads that existed only as a side effect of a mutation.
`GET /api/v2/workflows/{id}/deployment` publishes the state the deploy,
undeploy, and rollback responses carry, plus `needsRedeployment` — which
those responses structurally cannot carry, because they answer at the
moment the draft and the live version are equal. A caller that lost the
mutation response, or that polls from another process, had no way to ask.
Reuses `readWorkflowDeploymentStatus` behind `workflows.read`, the same
use case the internal status and deploy GETs already adapt.
`DELETE /api/v2/files/{fileId}` was a soft delete with no way to see what
it archived and no way to reverse it. `GET /api/v2/files?scope=archived`
pages the archived set and `deletedAt` on the file resource dates each
one; `POST /api/v2/files/{fileId}/restore` reverses the delete through
the existing `files.restore` operation. Restore is not a pure undo — it
returns the file to the root and renames it on a collision — so the use
case now reads the file back and both the response and the OpenAPI
description say what actually came back rather than what was deleted.
`scope=all` is rejected on the list for the reason the internal contract
already gives: it drops the `deleted_at` predicate and cannot use the
partial index. `scope=archived` combined with `folderPath` 404s when the
containing folder was archived too, which the contract documents.
* fix(v2): keep the unresolvable-cursor rejection a 400 on every surface
The cursor rejection lived in shared billing core but was an OrchestrationError
only, which the session-only GET /api/users/me/usage-logs cannot project: that
route is raw withRouteHandler and readTypedError matches instanceof HttpError,
so any signed-in caller typing ?cursor=x got a 500. UnknownUsageCursorError is
an HttpError carrying the OrchestrationError as its cause, so the v2 route still
renders BAD_REQUEST off the cause chain and the internal route answers 400.
Also closes the other half of the run-list parity: an inverted window on
GET /api/v2/logs is now a 400 instead of a silently empty page.
* fix(v2/tables): sweep union bodies per member and name the shapes on a rows 400
Review follow-ups on the strictness work.
The sweep was vacuous on the one union body it covers. Parsing
`{ notAContractField: true }` against `v2CreateTableRowsBodySchema` and looking
for `unrecognized_keys` anywhere in the issue tree is satisfied by either member
alone, so dropping `.strict()` from the single-row branch shipped green —
reproduced, 36/36 passing with the regression in place. The sweep now flattens a
union body into its members and asserts each one separately; removing `.strict()`
from either branch now fails a case that names it.
`POST /rows` answered an unknown key with `Invalid input`, the exact message the
v2 conventions name as failing the actionable-error rule, because a union
surfaces `invalid_union` first. The union now carries a message naming both
accepted shapes; the per-member failures still ride along in `details`.
Two TSDoc corrections. The `required` docstring claimed the domain rejects
turning the flag on over rows with empty cells — true of the update path, false
of add-column, which applies the flag as given (the same shape `unique` already
had here). And `.strict()` binds the top level only, so the view `config` object
and the shared sort-spec elements still strip unknown keys; both docstrings now
say so instead of implying full coverage.
* fix(v2): classify MCP discovery failures by type, not by substring
The tool-discovery error policy consumed categorizeError's status, whose
fallback is a substring match on the upstream message. Three consequences,
all caller-visible:
- A ZodError from the builder's own response `.parse` contains `invalid_type`,
so a Sim-side response-schema defect answered 400 "Invalid request
parameters" and suppressed the builder's 500 and its unhandled-error log.
- An upstream `Invalid params` or `not found` became the caller's 400/404 on a
request the contract had already validated.
- A stale OAuth grant to the third-party server answered 401, the status this
surface reserves for a missing or invalid Sim API key, so a client would
rotate a credential that was never the problem.
The policy now dispatches on the MCP error families and returns null for
anything else. Reauthorization is a 409 carrying
`details.code: MCP_SERVER_REAUTHORIZATION_REQUIRED`; an unreachable, slow, or
cooling-down server is a 503 with a constant message.
Also: widen the shared server path-param description now that it covers tool
listing, map the list query explicitly so no undeclared `cursor` reaches the
use-case input, and document the endpoint's write side effects.
* merge: bring in the MCP tool plane workstream
* feat(v2): make knowledge tags usable and let documents be updated
v2 accepted tag slots on upload and filtered search by tag display name,
but no response ever returned a tag value and nothing listed the
vocabulary, so a shipped feature dead-ended in the public API. A document
that failed processing could only be deleted and re-uploaded, and
retiring 500 documents cost 500 requests.
- GET /api/v2/knowledge/{id}/tags returns the vocabulary (display name,
slot, field type) as a full-set list.
- Document list and detail responses carry `tags`, keyed by display name
exactly as search keys its result metadata. Writes stay slot-keyed; the
tags endpoint is the mapping and the contract documents the split.
- PATCH /api/v2/knowledge/{id}/documents/{documentId} renames, enables,
disables, retags, or requeues processing. Derived indexing state is not
writable: asserting `processingStatus` on an unindexed document would
corrupt search. A retry may not ride along with field updates.
- PATCH /api/v2/knowledge/{id}/documents bulk-enables or bulk-disables.
Bulk delete is deliberately absent — that operation records no semantic
audit, and a public bulk delete would empty a knowledge base leaving no
DOCUMENT_DELETED entries.
- The document list accepts the same name-based `tagFilters` as search;
the name-to-slot resolver moves out of search into a shared helper, and
the filters are stamped into the offset cursor scope so a replayed
cursor cannot cross a filter change.
- Search accepts `rerankerEnabled`, `rerankerModel`, `rerankerInputCount`
and returns `rerankerScore`; `rerankerApiKey` and `skipUsageBilling`
stay unexposed. Every result now names its `knowledgeBaseId`.
knowledge.tags.list flips from workspaceApiKey 'deny' to 'allow' (and
gains the workspace_api_key principal kind) so it matches the sibling
reads knowledge.documents.list / read / search. The vocabulary is
required input for two operations a workspace key can already perform.
Every tag write stays human-delegated.
* fix(v2): name every 403 cause, unfork boolean params, close nested strictness holes
Four cross-cutting consistency gaps on the v2 public surface.
**403s now carry a machine-readable cause.** The conventions skill mandated
`error.details.code` on 403 and nothing emitted one, so a client had to
string-match prose to tell "raise this member's role" from "this workspace
refuses personal keys" from "buy an enterprise plan" — four different
remedies behind one status, and every message reword a silent break. The
vocabulary is a closed set, `FORBIDDEN_DETAIL_CODES`, with a `Record` of
descriptions beside it that the generated OpenAPI 403 description is built
from, so a code cannot reach the wire unpublished. Refusals throw
`ForbiddenOperationError` in the domain and `v2CaughtOrchestrationError` —
the function every v2 error policy falls through to — attaches the code, so a
route cannot forget it. The audit-log resolver distinguished four causes and
collapsed them into one; it now names each.
Cross-tenant refusals deliberately get no code: they are concealed as 404 and
naming their cause would hand back the existence signal the concealment
withholds.
**Two boolean query params rejoin the majority.** `?includeDeparted` and
`?includeOutput` were `'true'`/`'false'` string enums inherited from the
internal shapes they reused, while four sibling params were real booleans.
Both move to `booleanQueryFlagSchema`, which still coerces both strings — a
strict widening, so an existing caller is unaffected, and the spec stops
telling callers to send a string.
**Two nested strictness holes close.** `.strict()` binds the top level only,
so `sort: [{ field, direction, nulls: 'last' }]` was answered 200 with the
null-ordering request dropped, and an unknown key inside a saved view's
`config` was accepted and discarded — the headline `filter` bug one level
down. `sortSpecSchema`'s element and both view-config schemas are now strict.
Safe on the read side because `normalizeStoredViewConfig` projects the
schemaless stored blob onto the declared keys first, so a legacy row cannot
turn into a 500.
The two sort dialects stay as they are. `/logs` and `/workflows/{id}/runs`
have one sortable column, so there is no `sortBy` to pair with; renaming
`order` breaks every caller and an alias is a second spelling of one thing
with undefined precedence. Both contracts and the skill now state the rule.
* style: format the files the workspace-scoped lint gate does not reach
`turbo run lint:check` runs `biome check .` per workspace, so `scripts/` at the
repo root is outside the graph and four changed files were unformatted — one of
them a merge artifact from reconciling the route baseline across branches.
* fix(v2): collapse the four knowledge document projections onto one null-tolerant summary
Extracts toV2DocumentSummary in app/api/v2/knowledge/utils.ts and composes the
list, upload-acknowledgement and detail presenters from it. toV2TaggedDocument
serialized uploadedAt with a bare .toISOString(), so a document with no upload
timestamp threw where every sibling returned null and the contract declares the
field nullable.
Also consolidates the two Zod strictness walkers onto one shared introspection
helper that unwraps wrappers and expands unions, closing the hole where a
union-shaped schema answered null and was skipped by the pagination sweep.
* fix(v2): stop HEAD driving MCP discovery, and unbreak the updatedAt keyset page
B1: Next aliases HEAD onto GET, which RFC 9110 permits only because GET is safe.
The MCP tool-discovery GET is not: it opens a live connection to the registered
endpoint and writes the outcome onto the server row. The v2 JSON builder gains a
headSafe option, default true, and the discovery route declares itself unsafe —
a HEAD is authenticated and rate-limited, then answered bodiless.
B2: a discovery status write stamped updatedAt, which this branch added as a
keyset sort, so any concurrent discovery duplicated and skipped servers across a
caller's pages. Discovery liveness already has lastConnected, lastToolsRefresh,
lastError and statusConfig.
B4: a public refresh now skips the positive cache but keeps the failure cooldown,
so it cannot be used to drive a connection attempt per request at a failing
endpoint. An explicit user action on their own server keeps the full bypass.
B6: the consecutive-failure counter is incremented SQL-side rather than read,
incremented and written back, and the success branch carries the same workspace,
liveness and staleness guard the failure branch already had.
* fix(v2): bound the bulk update echo, close the search leak, and make the docs true
B3: a selectAll bulk document update echoed every changed identifier, which the
request does not bound — a 100k-document knowledge base produced a multi-megabyte
array, materialized and then element-wise validated. The use case now reports
whether the selection was unbounded and the presenter omits the echo.
A1: the knowledge search presenter spread the whole use-case result, which also
carries userId, workspaceId, a cost breakdown and a live secret-trace registry.
Only Zod's default key-stripping kept them off the wire. Projected explicitly.
P1-a: GET /knowledge/{id}/tags advertised all 17 slots while the document PATCH
accepted only the seven text ones. The writer already coerces every slot type,
so the PATCH now takes all 17 in their declared types, with a 400 where a
malformed value used to silently clear the tag.
P1-b: both new PATCHes deny workspace API keys and now say so.
P1-c: the two table query reads declare maxBodyBytes and now document the 413.
P1-d: getWorkflowDeploymentV2 loses its legacy suffix.
C3: deletes two orchestration error mappers with no callers that mapped
'forbidden' with no details.
D2: a stored null in table_views.config survived the pick and failed the
response schema.
Also folds the six 'bounded set' paraphrases onto one FULL_SET_LIST constant,
shares the run-window date bound between the logs and runs lists so their
documented parity is enforced rather than asserted, adds the missing barrel
export for FORBIDDEN_DETAIL_CODE_DESCRIPTIONS, and strictens two response
schemas whose peers were already strict.
Migrates 40 v2 route tests onto the shared @sim/testing harness: 26 asserted a
rateLimitSubjectIds shape v2 auth never returns, 26 asserted the wrong
refillRate, 33 could not exercise their 401 path at all, and 6 hard-wired the
rollout gate to null.
* fix(mcp): bound the connect handshake, and stop the 403 description over-claiming
B5: the connect clamp was getMaxExecutionTimeout(), the workflow ceiling of
seven days, so the real bound became the server row's own timeout — which the
registration contract permits up to 300s — times the connect retries. A slow
server could hold a Node request for roughly twenty minutes. Connecting is not a
workflow run, so the handshake now shares the one-minute ceiling tools/list
already applies to itself.
C2: the generated 403 description asserted that error.details.code names the
cause on every 403. Nine domain refusals still throw a bare forbidden
OrchestrationError and reach the wire codeless, so the wording now says 'where
the cause is one a caller can act on'. Reparenting those throws is left as a
deliberate change: one of them is a cross-tenant refusal that belongs in the
codeless class and would change its status.
* chore: reconcile the route ratchet with staging
* style: sort imports and format the three files biome flagged
* fix(openapi): import the forbidden-code constants from their module, not the application barrel
The barrel also re-exports the authorized use-case layer, which loads
@sim/db at import time. That pulled a database connection into the
OpenAPI spec check, so check:audits failed wherever DATABASE_URL is
absent, including CI.
…er (#6644) * fix(workflow): stop a nested block jumping when it leaves its container `getNodeAbsolutePosition` added the container's header and padding to a child's position. Those are already in the position: React Flow places a child at its parent's origin plus its own coordinates, and `clampPositionToContainer` is what holds it clear of the chrome, flooring it at `LEFT_PADDING` and `HEADER_HEIGHT + TOP_PADDING`. Counting them twice put every nested node 16px right and 66px below where it actually renders. Visible as a block dropping down-right the moment it is dragged out of a Loop, and as a block landing off-target when dragged into one from the canvas. Also skewed container hit-testing during a drag and the bounds `fitView` focuses on. Two callers already knew: both subtracted the same three constants straight back off to recover a relative position. They now take the difference of two absolutes, which is what a relative position is. A third place, React Flow's child `extent`, had its own copy of the numbers — a fourth distinct header height, 42, against the 40 the card renders — and now reads the same constants as the clamp, so a drag stops where a drop would put it. `positionAbsolute ?? getNodeAbsolutePosition(...)` in the fit-view path can also stop disagreeing with itself: React Flow's own answer carries no offset, so the two branches returned points 66px apart for the same node. * refactor(workflow): type the node-utilities block map `useNodeUtilities` took `Record<string, any>`, so the test fixtures had to be cast to reach it and nothing in the hook was checked against a real block. Typing it as `Record<string, BlockState>` surfaced an unsafe read straight away: the cycle walk re-read `blocks[currentId].data.parentId` after the `while` condition had tested the same optional chain, on a map where both links are optional. It now reads the value once and breaks on absence, which is what the condition was trying to express. The fixtures follow the hook's own parameter type, so they stay honest without a cast on either side.
* fix(workflow): draw a highlighted edge over the ordinary ones An edge's z came from the nesting depth of the container it belongs to, and a highlighted edge kept that depth like any other. A line one level deeper therefore sat above it and painted straight through the highlight, cutting it in half wherever the two crossed. Give a highlighted edge — selected, or connected to the selected card — the top tier of the edge band instead. Depth only ever ordered edges against each other, and once the user has picked one out, being drawn whole matters more than which container it came from. The tier stays inside the band, below the cards, deliberately: highlighted edges were elevated over the cards once before and drew across the chrome of their own endpoints. A line belongs behind cards, knobs and the action-bar swell whether or not it is highlighted, so ordinary edges give up the top of the band rather than the band being widened into the cards. * fix(workflow): elevate the connection preview edge with the rest It renders highlighted — its data carries `isConnectedToSelection` — but it was the one call site left taking a depth tier, so the line being drawn could be crossed by an ordinary edge in a deeper container. Highlighted now means elevated with no exception. Also drop the export on the highlighted tier: nothing outside the module reads it, and the band's tiers are an implementation detail of `getEdgeZIndex`. * fix(workflow): give the edge highlight one definition The z-index elevation I added checked canvas selection only, while the edge darkens for panel focus too — a block open in the editor lights its edges, and those stayed depth-tiered, so an ordinary edge could still cut through the highlight. The bug I set out to fix, on the path I had not covered. The condition already existed in two places and the second one carries a comment saying it must mirror the first exactly, because a knob checking fewer conditions than the line leaves a dark line running into a light knob. Adding the z would have made a third copy, and the finding here is what the third copy gets you. One predicate now, in `edge-highlight`, used by the line, the knobs, and the z. The canvas subscribes to the panel store rather than reading `getState()`, since the z has to be recomputed when the open block changes.
* fix(billing): checkout guard, admin panel case * fix(billing): serialize checkout admission * fix(billing): release checkout admission claim
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
Too many files changed for review (333 files, 100 file limit). Bypass the limit by tagging |
PR SummaryMedium Risk Overview Models & xAI: Grok 4.6 is added as the xAI flagship with reasoning effort wired through the Grok adapter; agent docs list xAI models with streamed thinking. Integrations: Windchill (PTC WRS 2.7 document OData) is documented with a full integration page, icon, and meta entries. Workflow editor: Subflow sizing/jump fixes, dual-mode blocks inside loop/parallel, edge layering, running-hatch styling, knob geometry, and consistent block tiles in listings. CI: Desktop release job no longer skips when a transitive Other: Copilot file-preview/tool-args and document render failures; cmdk focus/fog; desktop prod CI; blog provenance post; blocks tile consistency. Reviewed by Cursor Bugbot for commit 9f8d4d1. Configure here. |
* fix(tools): sanitize database execution errors * fix(tools): retry transient permission failures * fix(tools): preserve preflight cancellation
… rule (#6648) * improvement(emails): size the header wordmark to the landing navbar's rule The email header rendered the wordmark at 34px of ink against 16px body copy, and inked it #1a1a1a while every other line of the email uses #434343. Size it by the rule the landing navbar states: the mark stands 2px above and below its neighbouring text (18px against 14px chip labels), so 20px against the email's 16px body copy. Rasterize it from the same brand outlines the navbar renders, filled with the email's own textBody token, so the two surfaces cannot drift. * chore(emails): drop the wordmark generator script The raster is committed at 4x, so the display box can be retuned without re-exporting it — the script only ever ran by hand, and the test pinning the asset to an exact multiple of the box would have forced a pointless regeneration on any size tweak. Keep the shared outlines the navbar and 8 other surfaces already render, record how the asset was produced on the size constant, and assert the property that actually matters: the asset out-resolves its display box. * chore(branding): re-export the branding barrel through the absolute alias Matches the repo's absolute-import rule and the majority of lib barrels (billing, table, uploads and five others), instead of leaving this file split between relative and absolute re-exports. * docs(branding): correct the wordmark comments that named the removed script Two TSDoc blocks still pointed at scripts/generate-email-wordmark.ts as how the email raster is produced. Point them at the committed export and the size constant that records its fill and scale instead.
* fix(copilot): align principal lifetime with orchestration * fix(copilot): align workflow lifetime expectation
* fix(enrichment): require work email company domain * fix(enrichment): show exhausted cascades as not found
#6648 shrank the header wordmark by re-exporting the raster edge-to-edge at 168x80, replacing a 272x164 file whose mark was inset. Same URL, new proportions — so every email already delivered, which keeps the width=68 height=41 it was sent with and refetches that URL forever, now stretches the mark ~20%, and a client still holding the old bytes squashes it ~22% into the new box. That is the squished logo on staging. Re-export onto the original 272x164 canvas instead, with the outlines at their own aspect (the previous asset was itself 3.9% squashed) and the textBody fill. Old mail renders against its own numbers as before; the header takes a 43x26 box for 20.7px of ink. A test pins the canvas, since that shape — not the display size — is what old mail depends on.
…type token (#6653) * improvement(docs): make the API reference read as code, and unify the type token The API-page font override matched every span/div/p inside the page, which outranks the .font-mono class on specificity, so every parameter name, type, and identifier silently rendered in the body sans face. Exclude .font-mono so code tokens stay monospace. Consolidate the three divergent type-slot treatments — plain scalar, union, and schema reference each carried their own chip definition, differing in size, weight, face, and box height — onto one code token that reuses the docs inline-code recipe and the platform's 20px chip height. Demote the row metadata: 'required' and 'header' were filled pills, 'required' on the error token, making a constraint the loudest element on the page and a page of required parameters read as a page of alarms. Both are now uncontained text, leaving the type token as the only box on the row. Pin the two 'application/json' labels to one treatment; the Request Body and Response headers rendered the same string at different weights and faces. * improvement(docs): mono status-code tabs, and match fumadocs' lucide icons to emcn Status codes in the example panel are numeric literals and render as code everywhere else on the page, including the Response header's own trigger, but fumadocs rendered the strip in the body sans face. Language tabs sit in a separate container and stay sans — those are product names, not code. fumadocs draws a few lucide glyphs on API pages that its client-component overrides do not expose (the heading anchor and the code-block copy button). emcn strokes at 1.55 and lucide at 2, so those icons read heavier than every icon around them; match the weight. * fix(docs): align the auth type chip with every other property row, and wrap example code The auth row collapses its real `<token>` type and renders the chip through ::after, so the span is only a wrapper — but it still matched the type-token rule and kept that rule's border, height, and gap. The border drew a second empty box around the real chip and the gap opened in front of it, because the collapsed text remains an anonymous flex item; together they pushed the chip right by roughly 8px that no other row had. Example-panel code overflowed sideways instead of wrapping: fumadocs sizes the block with `w-max`, so it grew to its longest line inside a 400px scroller and the existing pre-wrap never applied. Cap the width, switch break-all to overflow-wrap anywhere so only unfittable tokens split, and reserve room for the copy button fumadocs floats over the first line. * revert(docs): let example code overflow instead of wrapping Wrapping restarts every continuation line at column zero, and in a JSON body indentation is what carries nesting depth — so a wrapped response misreports its own structure. A hanging indent keeps the depth but needs the shiki lines forced from flex rows to blocks, which breaks the line rhythm. Removes the pre-wrap rules rather than repointing them: fumadocs sizes the block with w-max, so the previous rule never took effect and overflow was already the behaviour on the page. * fix(docs): tighten array type tokens and keep the union separator legible An `array<T>` slot holds its angle brackets as bare text nodes, which become anonymous flex items, so the slot's gap prised `array<` and `>` away from the type they wrap. Drop the gap and let the union separator carry its own margin; this also makes the auth row's gap override redundant. The separator was dimmed twice, by a muted token and again by opacity, which on the dark chip fill left `string | null` reading as `string null`. * fix(docs): restore the hidden API key description, and drop dead API-reference CSS The rule hiding the trailing `In: header` line matched `p:has(> code)`, which is a shape, not a target — every scheme description in our specs cites a status code, so the whole explanation of personal vs workspace-scoped keys was display:none on every API reference page. Match the last child instead, and shorten the description to one line now that it renders. The dropdown trigger's hover rule had been left below a new id-qualified base rule that outranked it, so the trigger could no longer change colour on hover. Removes what does not run: the four `::-webkit-scrollbar` rules (specifying a non-auto scrollbar-width makes Chromium ignore them, and Firefox never had them) and an `order: 2` block whose selectors and declaration the type-token rule above it already carried. Names the two values the API reference repeats — the monospace stack, written out eleven times, and the 12.5px code size, written nine — as --font-mono-stack and --text-code. Also drops four !important declarations that already won on specificity, a --text-muted fallback that can never fire, and a lucide selector subsumed by the one beside it. * refactor(docs): define the API-reference chrome once, and cut the commentary The metadata face — size, leading, weight, mono stack — was written out in seven rules that a comment asked future readers to keep in sync by hand; it is now one rule those seven consume, each adding only its own colour, content, and order. The auth row's chip likewise re-derived all eleven declarations of the type token and now joins that rule, keeping only its label. Comments were running longer than the rules they documented — 88 added comment lines against 73 declarations. Trimmed to the load-bearing facts: cascade traps, browser behaviour, and the bugs a rule prevents. Dropped the block narrating why the wrap rules were reverted, which duplicated its own commit message. Also retires a scrollbar token left unreferenced by the webkit removal, moves the last two fumadocs colours in our own components onto platform tokens, and brings the callout icon to 1.55 so the docs really do have one icon weight. * fix(docs): keep the union separator in the type token's own face The `|` between union members is a classless span, so the page-wide `span:not(.font-mono)` rule assigned it the body sans face while the members beside it stayed mono — one chip rendering in two faces. Applies the inherit reset to every descendant of a type token rather than just its links, so anything fumadocs nests there later is covered too.
- Seed the workspace list instead of prefetching it. The empty-list case was signalled by throwing inside queryFn, which the retry: 1 default re-ran the entire read to re-derive, a retry delay later. Log the failure path, which was silent — contract drift would have degraded into every viewer waterfalling with nothing in the logs. - Drop non-painted nodes from rrweb snapshots via slimDOMOptions. Enumerated rather than true/'all' so headTitleMutations stays off and replays keep document.title. Prefetch concurrency and await semantics are unchanged, so sidebar paint timing matches staging.
* fix(files): preserve principals when serving documents * fix(files): address principal serve review findings
…ocument on open (#6652) * fix(files): stop the collaborative editor rewriting and reflowing a document on open Opening a file rewrote it. Binding an editor to a seeded document emits a Yjs update of its own — ProseMirror appends an empty paragraph to any doc that does not end in one — which the relay saw as a real edit and persisted. Every open therefore uploaded the file under a FRESH storage key and deleted the old one, 404ing the page's own in-flight content read, bumping "Last Updated" just from viewing, and churning a blob per open. Worse, a trailing blank line cannot serialize, so the file never recorded that paragraph and nothing reconciled the two: each client that seeded without seeing another's contribution stacked one more. A real document reached 18 against the placeholder's 1 — measured as the pane growing several hundred pixels the instant the live editor took over. - Seed and merge through the editor's own normal form (`editorNormalForm`), so binding is a no-op and `canonicalizeYDoc` collapses an accumulated run back to one. Placed at the collab boundary, not in `parseMarkdownToDoc`: only the CRDT has to agree with the editor — every other consumer of the parse renders through a real editor that normalizes itself. - Skip a persist whose projection already matches the durable bytes. Byte length is the free reject, so the compare read only happens when a no-op write is actually on the table. - Revoke collaborative readiness on a fatal join. The sticky `syncedOnce` latch outlived the document: after a readiness timeout the provider drops `synced` so the gate closes, but the latch re-opened it on the offline fallback's seed flag — handing back an EDITABLE editor on a document the provider had abandoned, with client autosave gated off because collaboration is nominally on. Keystrokes went nowhere and vanished on reload, with no error shown. - Recover from a superseded storage key instead of stranding the reader: a 404 re-resolves the file record, so the read re-keys onto the current object. And do not focus-refetch durable bytes while the relay owns durability. - Prefetch the workspace file list in the layout, where the sidebar already reads it. `HydrationBoundary` defers an already-seen query to an effect that SSR never runs, so a page-level prefetch of that key could not reach the server render — the file route rendered a spinner and disagreed with the client about the header's markup (a hydration mismatch). - Load the document font with `display: block`. A swap repaints prose in metric-adjusted Arial first, so paragraphs re-wrap when the real face lands. Also: a detail-route `loading.tsx` (the segment was inheriting the list chrome), `normalize.ts` renamed to `field.ts` now that it holds only the field constant, and the duplicate `COLLAB_DOC_FIELD` in the streaming path folded into it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(files): serve a whole document on join, and stop persistence deadlocking Opening a file right after an edit replayed it — the block you had just moved moved again, in front of you. A room rebuilds itself from the file's Redis stream one entry at a time, into the same Y.Doc that fans every update out to its room, and the join attached the socket before that finished and before the server seed. So a client was never sent the document; it was sent the document's history, and it watched that replay. The new join-readiness test reproduces it exactly — ['AAA', 'BBBAAA'] where one state was due — and fails without the fix. Underneath it, persistence had deadlocked. The If-Match token is a REMEMBERED timestamp: held in the room, which dies with it, and in a cluster key written fire-and-forget. A relay that exits in the moments after a successful write comes back holding a version older than the file's, every persist then fails the CAS, and because a conflict neither writes nor advances the token, that document can never be persisted again. Measured on a live file: token 1786594348911 against a content version of 1786594418409, with 103 unpersisted entries still in the stream. The durable markdown froze there — and the editor's placeholder is built from it, so every reload painted the pre-edit document and the live one corrected it on screen. - Assemble a room before attaching a client to it. The join awaits the stream catch-up and the seed, so the first sync IS the finished document, in one message. The seed is memoized on the room, so concurrent joins wait for the same one instead of the second being served an empty doc; a task that loses the seed lock PULLS the winner's seed from the stream rather than waiting for the tailer to push it, which is what left a freshly uploaded file read-only until its readiness deadline lapsed. - Drop the client-side quiet-frame gate that was standing in for this. It was unsound in both directions: it delayed a document that was already correct, and it opened mid-flight anyway whenever updates arrived more than a frame apart, which is what a cross-region Redis and a long room history produce. - Await the cluster version write after a successful persist. It is the only record that survives a teardown, and one round trip after a blob write is not a cost worth a wedged document. - On a version conflict, ask the CONTENT, not the clock. If the file still holds the bytes this document last projected — the tag written with every persist — then nothing out-of-band exists to protect, so re-sync the token and write. Any other bytes and the conflict stands exactly as before. - Actually run the stale-storage-key recovery. It was requested from inside the failing read's own queryFn, where react-query drops it, so nothing re-resolved the record and the reader sat on a dead key showing "Failed to load file content" until something unrelated refetched. It now runs off that cycle and cancels a read already in flight, which could only hand back the dead key. While the record is re-resolving the surface reports loading, not failure. - One document per file, for its whole life. A document rebuilt from markdown is a DIFFERENT document to Yjs — its items carry new client ids — so a client still holding the old one merges the file into itself, twice, on both sides. The seed now stores what it builds (until that row existed, a file opened but never edited was rebuilt on every open) and resumes it with a CRDT diff when the markdown moved on out-of-band. A document also carries an identity the join ack names, so a tab that outlived its room is refused rather than merged. - Let a browser keep an embedded image. The inline route sent no-cache with no validator, so every open re-downloaded the whole image — measured at ~1 MB per open of a real document, with the image area blank until it landed. A `key` names one storage object and a content write never rewrites one, so those bytes are immutable; a `fileId` names the file, whose bytes move, so that form still revalidates. * fix(files): name every collaborative document, and only cache what the URL names Two findings from review, both real. A document stored before identities existed is returned by the seed's fast path on every open, and that path never named one — so those files could never acquire an identity, and the join-ack guard could never fire for them. That is the population most likely to have a tab that outlived its room, which is the case the guard exists for. The fast path now names an unnamed document and stores it, once: minting without storing would name it differently on every open and the guard would start refusing clients that hold the very same document. The inline route marked a response immutable whenever the caller passed a key, but a key is resolved to a FILE and the file's current key is what gets streamed. A content write landing between those two reads would serve the new bytes under a URL naming the old object — and cached for a year, that is wrong forever. The flag is now what it always meant: the URL names the exact object that was streamed. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…render (#6657) * perf(prefetch): read the data layer instead of calling our own API over the wire Four server-render prefetches went out over HTTP to our own routes. With INTERNAL_API_BASE_URL unset in prod, getInternalApiBaseUrl() falls back to the public base URL, so each was RSC -> public HTTPS -> load balancer -> back into the app, awaited inside the render with a second round of auth. - /home fetched the workflow folder list that the workspace layout had already fetched, under the identical query key. Since getQueryClient() builds a new client per call on the server, the two never deduped: same data, twice a request, once directly and once over the wire. Dropped; the layout's entry already hydrates it. - /home cached raw route JSON under workspaceFilesKeys.list, while files/prefetch.ts seeds that same key from listWorkspaceFilesWithShares. The contract declares the date fields z.coerce.date(), so consumers hold Dates — a file record's type depended on which page the viewer landed on. Now reads the same function files/prefetch.ts does. - tables and knowledge folder reads now call listFoldersForWorkspace, matching the sidebar prefetch. These reads carry no authorization of their own, so each surface proves the viewer through getWorkspaceHostContextForViewer first and caches nothing when it fails, leaving the client fetch to reach the route for the real 403. Both it and getSession are cache()d and already resolved by the layout, so the proof costs no extra queries. Left on the wire, deliberately: the tables and knowledge lists, whose cached shape is the serialized wire shape, and pinned items and members, which have no exported data-layer function. * improvement(prefetch): skip the viewer proof when there is no session Passing an empty-string userId ran a real permission query that could only return null. Take an optional userId instead and skip straight to the unauthorized path, matching how the home prefetch is called. * perf(prefetch): finish removing self-HTTP prefetches and delete the legacy helper Converts the last four server-render prefetches that called our own API over HTTP, and deletes prefetch-internal-fetch.ts now that nothing imports it. - knowledge bases: runs the route's own listInternalKnowledgeBases use case with a principal from the same internalSessionAuth policy the route declares, then projects through the same presenter and contract. Not a bypass of the application boundary — the same path, called in-process. - tables: extracts the route's list projection into lib/table/wire.ts as toTableListItem, which the route and the prefetch now both call. This matters because listTablesContract's response schema is a passthrough z.custom, so a client fetch caches the route's JSON verbatim. Seeding listTables() directly would have put Date objects and the server-only metadata field under a key the hook never sees them on. - pinned items: extracts the route's inline query into lib/pinned-items/queries.ts as listPinnedItemsForUser, which the route now calls too. - workspace members: getWorkspaceMemberProfiles already existed; the prefetch calls it directly. normalizeColumn moves from app/api/table/utils.ts to lib/table/wire.ts with ten importers repointed. That also removes a pre-existing lib/* -> app/api/* boundary violation in lib/table/import-runner.ts. No response shape changes: the v1/v2 edits are import-path moves only. Every converted read proves the viewer first and caches nothing when that fails, so an unauthorized viewer's client fetch still reaches the route for the real 403. Authorization equivalence was checked by unfolding both paths to checkWorkspaceAccess rather than assumed. * improvement(prefetch): collapse the duplicated folder prefetch and unify the call shape - Extract prefetchResourceFolders. The same eight-line folder prefetch was written three times, varying only by resourceType, with the key, stale time and mapper kept in sync by hand. - Adopting it removes the conditional spread from the tables and knowledge prefetches. Tables can now early-return, matching prefetchFilesBrowser: prefetchResourceListChrome already self-guards on the same cached host context, so a null context meant the function did nothing either way. - Take userId as string | undefined everywhere and guard inside, so every prefetch module has one calling convention rather than two. - Export toWireTimestamp and use it for the create-table response's own copy of the same idiom, and drop a cast that the extraction made dead: the parameter is already TableDefinition, whose schema is TableSchema. - Read params and the session concurrently on the tables and knowledge pages, matching the files page, and drop TSDoc that restated each prefetch's own. * fix(prefetch): keep the tables list on its route and cut the executor edge Reading listTables from a page prefetch put the executable tool registry into the Tables page server graph — ~4,700 modules, which check:tool-registry-boundary rejects. lib/table/service reaches workflow-columns by several independent paths (directly, and through jobs/service and rows/service), so severing one edge is not enough; untangling that belongs in its own change. - The tables list goes back through GET /api/table, with the reason recorded so the next person does not repeat the attempt. Folders and chrome on that page stay on the data layer. - stripGroupDeps moves to its own leaf module. It is a pure projection over a WorkflowGroup, but living beside the group runtime meant every importer of lib/table/service paid for the executor to get it. Net effect on the Tables page graph: 2,186 modules to 1,742. * perf(prefetch): finish the migration, delete the legacy helper, ratchet page graphs Answers the question the previous commit left open: the tables list did not have to stay on HTTP. lib/table/service reached the executor through jobs/service -> rows/service -> workflow-columns, for one symbol. pendingDeleteMask is a delete-visibility SQL clause with no executor involvement, so it moves to its own leaf and that chain is cut. The tables prefetch now reads the data layer like every other one, and prefetch-internal-fetch.ts is deleted: nothing in the app calls its own API over HTTP during a server render any more. stripGroupDeps likewise moves to a leaf rather than being re-exported through workflow-columns, so its importers no longer pull the executor to get a pure projection. React Query mechanism fixes, all found by audit: - settings/[section] fired two prefetches without awaiting them. Only a settled query is dehydrated, so those were shipped mid-flight; a rejection hydrated into an error state retryOnMount: false never retries, leaving the panel broken for the session. Awaited now, and the pending-dehydration opt-in is removed since nothing streams. - The viewer profile was prefetched by both the layout and the settings page. Separate server QueryClients mean that was a real second read per request. - prefetchSubscriptionData was dead, and hand-rolled an unannotated raw fetch. - retry is scoped to the browser. Query core defaults it to 0 on the server; stating one value for both opted awaited prefetches into a retry backoff. The gcTime default is dropped entirely — 5 minutes is already the browser default, and setting it explicitly overrode the server's Infinity, leaving a live timer and payload per request. check:tool-registry-boundary now also ratchets per-page module counts against a committed baseline, attributing a regression to the import that caused it via a dominator tree. It caught a +444 regression in this branch by hand; it would have caught it in CI. Its import regex also missed bare side-effect imports, so `import '@/tools/registry'` could have slipped past it entirely. Prefetch guidance added to .claude/rules/sim-queries.md. * fix(prefetch): correct the extracted module's db imports and stale rationale Audit findings from the migration. - pending-delete-mask imported its schema tables from @sim/db rather than @sim/db/schema, which the module it came from was careful to split. The global test mocks are bound per-entrypoint and only the schema mock exports tables, so every suite that reaches pendingDeleteMask would have failed on a missing mock export. Restored to the original convention, and the same split applied to the new pinned-items queries module before it grows a test. - The settings prefetch and page justified awaiting with a mechanism this branch removed — pending queries being shipped with their promise. Only a settled query is dehydrated now, so an unawaited prefetch is dropped from the payload entirely. Same conclusion, correct reason, and no longer contradicting the rule this branch added. - Removed the doc block left orphaned above validateSchema when stripGroupDeps moved out of workflow-columns. Skill projections regenerated after trimming the boundary skill. * chore(table): drop a section separator comment Separators like these are non-TSDoc decoration that CLAUDE.md already rules out. This is the only one in a file this branch touches; the rest of the repo is swept separately. * chore: remove section separator comments CLAUDE.md already rules these out ("No ==== separators. No non-TSDoc comments"), but 546 of them had accumulated across 48 files. They decorate rather than explain, and they drift: a separator says "Validation" while the code beneath it moved elsewhere, as one in workflow-columns already had. Pure deletion — no source line was touched, and lines inside template literals were skipped so nothing in a generated string changed.
… could not fail (#6659) The audit found the seed contract — the whole point of #6656 — had no test, and that one existing assertion was vacuous. - prefetchWorkspaceSidebar and seedWorkspaceList now have coverage: the empty list seeds nothing (so the client reaches the route's default-workspace creation path), a populated list seeds, a rejected read neither throws nor seeds, and a host context for another workspace seeds nothing at all. Verified falsifiable — removing the empty-list guard turns the first red. - The graceful-failure row for prefetchFilesBrowser asserted on the file-list key, which that function deliberately never writes, so it held no matter what the code did. It now asserts the folder key it owns, and the setup rejects the folder read. - The sidebar was the third hand-rolled copy of the folder prefetch the shared helper was extracted to remove; it now calls prefetchResourceFolders too. - prefetchKnowledgeBases returns early without a userId like every sibling, rather than reaching the authenticator and throwing per render. - Dropped two docblock claims about a `retry` default that no longer applies server-side.
Uh oh!
There was an error while loading. Please reload this page.