fix(forks): stop sync demanding config the source never had - #6766
Conversation
Fork sync manufactured required re-pick rows the source never asked for, disabling
Sync and suppressing the "Fully mapped" badge. Two distinct causes landed on the
same line in `collectForkDependentReconfigs`.
Cause A - a block-level `required` applied to a nested tool param. The collector
runs one helper over both a block's own subblocks and the params of each tool in a
`tool-input`, reading the raw block config for the nested pass. A Jira Get Issue
tool inside an Agent block therefore inherited Jira's `issueKey` required-condition
even though the tool param is `visibility: 'user-or-llm'` - the agent fills it at
runtime, and `createLLMToolSchema` keeps an empty one in the model's schema
precisely so it can. The tool-row editor already strips `required` for anything not
`user-only`; the fork collector was the only surface that did not.
Cause B - demanding a value the source never had. `projectId` hangs off the
credential anchor, so a Jira block on `write` with a blank project emitted a
required row. The proof it is a collector bug: only members carrying a
`selectorKey` are emitted, so the basic selector gated while its advanced twin
(identical `required`, no `selectorKey`) did not - the same empty config blocked or
not purely on a display preference. That is also why toggling to manual mode
"fixed" it.
Neither fix subsumes the other: an emptiness guard alone leaves a *populated*
`user-or-llm` param gating after a parent swap (`effectiveDependentValue` blanks it
when the parent changed), and the visibility rule alone never touches top-level
subblocks.
Both invariants now meet in one predicate, and the tool-row rule is extracted so
the editor and the sync gate cannot drift. Rows are still emitted either way - only
the gating flag changes - so no stored dependent value is orphaned.
Also fixes the placeholder stutter in the re-pick selector ("Select select issue"),
which composed a verb onto titles that already read as instructions.
Verified end-to-end against a forked workspace: before, 3 of 7 rows were required
(including one with a populated source value); after, only a top-level populated
required field gates, Sync enables, and the badge reads "Fully mapped".
…e-required-blockers
… one Pre-landing review caught a silent un-gating in the new predicate: it asked `isNonEmptyValue` about `rawSourceValue`, which flattens every non-string to `''` for the wire contract. A multi-select dependent selector stores an array, so a populated one reported blank and stopped gating the sync. `isNonEmptyValue` handles arrays and non-strings deliberately - give it the raw value. Reachable today via zoho-desk `departmentIds`, the one multi-select dependent selector with a `selectorKey` + `dependsOn`. Also from review: - the canonical id is an alias, so it no longer clobbers a param that owns that key as its own `paramId` (first write wins) - drop a redundant conjunct that implied a third state the code cannot reach - document the present-but-undefined visibility case, which the resolver's `buildToolInputSearchConfig` branch produces routinely - extract the placeholder-noun transform so a bare "Select" title falls back to the whole title instead of rendering "Select " / "No found", and test it Tests: non-string and empty-array source values, both verified to fail without the fix; the basic/advanced parity case now pins both shapes rather than calling `.every()` on an empty array; the indexer mock is reset per test so the new cases are not order-dependent.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview Gating rules — Pre-sync dependent reconfig rows still appear for in-place re-picks, but Post-sync promote — Cleared nested tool dependents use the same visibility map from UI copy — Dependent field selectors strip leading “Select/Choose/Pick” from titles before building placeholders, avoiding strings like “Select select issue”. Reviewed by Cursor Bugbot for commit dbeba09. Bugbot is set up for automated code reviews on this repo. Configure here. |
Greptile SummaryThe PR fixes fork synchronization so blank source configuration and model-supplied tool parameters do not incorrectly block synchronization or redeployment.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| apps/sim/lib/workflows/tool-input/param-visibility.ts | Introduces shared visibility-aware requiredness helpers used consistently by editor and fork-sync paths. |
| apps/sim/ee/workspace-forking/lib/mapping/dependent-reconfigs.ts | Makes blank source dependents non-gating and applies authoritative tool-parameter visibility during pre-sync collection. |
| apps/sim/ee/workspace-forking/lib/remap/remap-references.ts | Applies the shared visibility rule to cleared nested tool parameters so promotion does not incorrectly suppress redeployment. |
| apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer.tsx | Reuses the shared user-supplied predicate to keep editor optionality aligned with fork synchronization. |
| apps/sim/ee/workspace-forking/components/fork-sync/dependent-field-selector.tsx | Uses normalized field nouns to avoid duplicated imperative wording in selector copy. |
| apps/sim/ee/workspace-forking/lib/mapping/dependent-reconfigs.test.ts | Adds broad regression coverage and converts the previously flagged declaration documentation to TSDoc. |
| apps/sim/ee/workspace-forking/lib/remap/remap-references.test.ts | Verifies that cleared model-supplied parameters remain surfaced without blocking redeployment. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
Source[Source workflow configuration] --> PreSync[Collect dependent reconfigurations]
Visibility[Resolved parameter visibility] --> PreSync
PreSync --> Gate{User-required and source configured?}
Gate -->|Yes| Require[Require reconfiguration]
Gate -->|No| Allow[Allow synchronization]
Allow --> Merge[Merge and remap workflow]
Merge --> Cleared[Collect cleared target dependents]
Visibility --> Cleared
Cleared --> DeployGate{Required cleared value?}
DeployGate -->|Yes| Withhold[Withhold redeployment]
DeployGate -->|No| Redeploy[Redeploy synchronized workflow]
Reviews (2): Last reviewed commit: "style(forks): use TSDoc for the new test..." | Re-trigger Greptile
Greptile caught a real asymmetry, and corrected a wrong assumption in the first commit: `needsConfiguration` is NOT warning-only. `promote.ts:1035` skips the target's redeploy for any workflow in that list, and `:792` also withholds its chat-deployment carry-over. So with only the pre-sync collector fixed, an Agent block whose Jira `issueKey` was populated in the target and cleared by a credential remap would let the sync through (the modal correctly treats a model-supplied param as non-blocking) and then silently decline to redeploy that workflow - leaving the fork running its previous deployed version with no gate and no error. Both collectors now resolve `required` through one shared `resolveToolParamRequired`, so the pre-sync gate and the promote path cannot disagree about what a nested tool param means. The lookup (sub-block id, then canonical param id, then fail closed to the block-level rule) lives in one place instead of being duplicated. The `@/tools/params` mock in remap-references.test.ts is now overridable so a test can opt into an authoritative resolution; its defaults are unchanged and the other 74 tests pass untouched. The new case is verified to fail without the fix.
CLAUDE.md requires TSDoc for documentation and no non-TSDoc comments. The vi.mock boundary note, the resolve helper, and the beforeEach mock-reset rationale all document declarations, so doc tooling could not associate them. In-body step comments are left as-is - they explain a flow, not a declaration.
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit dbeba09. Configure here.
What
Workspace fork sync manufactured required re-pick rows the source never asked for, disabling Sync and suppressing the "Fully mapped" badge. Two independently-reported symptoms turned out to be two distinct root causes landing on the same line in
collectForkDependentReconfigs.Symptom A — an Agent block whose tools include Jira Get Issue, with Select Issue deliberately blank so the agent resolves it at runtime. The modal demanded a value.
Symptom B — a Jira block on
writewhose Select Project was blank in the source. A required row appeared under the credential card. Toggling the field to manual-input mode and redeploying made the demand vanish.Why
Cause A — a block-level
requiredapplied to a nested tool paramemitAnchoredDependents()runs over two structurally different things: a block's own subblocks, and the params of each tool inside atool-input. The nested pass read the raw block config and stampedrequired: isSubBlockRequired(...).Jira's
issueKeysubblock declaresrequired: { field: 'operation', value: ['read', …] }, so a Get Issue tool evaluatedoperation === 'read'→required: true.But Sim already models this as
ParameterVisibility: the tool param isissueKey: { required: true, visibility: 'user-or-llm' }. Every other consumer honors that — the tool-row editor stripsrequiredfor anything notuser-only, andcreateLLMToolSchemakeeps an empty param in the model's schema so the model can fill it. The fork collector was the only surface that didn't.The editor's own verdict on the same field:
Domain *andJira Account *carry asterisks; Select Project and Select Issue do not.Cause B — demanding configuration the source never had
projectIdhangs off the credential anchor, so a blank one was emitted as a required row. The asymmetry that proves it's a collector bug rather than a data bug: only members carrying aselectorKeyare emitted, so the basic selector gated while its advanced twinmanualProjectId— identicalrequiredanddependsOn, noselectorKey— did not. The same empty configuration blocked or didn't purely on a display preference. That's also why the manual-mode workaround worked: the toggle writescanonicalModes.projectId = 'advanced', and the dormant-member guard skips the row.Not the remapper. A Jira project isn't a remappable kind —
project-selectormaps toselector-resource, absent fromREGISTRY_KIND_TO_FORK_KIND, so it never reachesunmappedRequired.Neither fix subsumes the other
user-or-llmparam gating after a parent swap, becauseeffectiveDependentValueblanks the stored value whenever the parent changed in-session.How
One predicate at the single
out.pushsite, expressing two independent invariants:tool-input, only auser-onlyparam is the user's to supply. Applies to the nested pass, where visibility is known.The tool-row rule is extracted to
apps/sim/lib/workflows/tool-input/param-visibility.tsand used by both the editor and the sync gate, so they can't drift. Visibility comes fromgetToolInputParamConfigs— the same resolver the rest of fork remapping already uses. Unknown schemas (custom-tool / MCP / unresolvable tool id) fail closed to the pre-existing gate.Rows are still emitted either way — only the gating flag changes — so no stored dependent value is orphaned and no row disappears.
The predicate is written as the positive
=== 'user-only', never auser-or-llm || llm-onlydenylist: the two non-user visibilities reach the same verdict for different reasons, and a denylist would silently mis-classify any visibility added later.Blast radius
~110 required selector dependents across ~45 blocks stop gating when blank in source. Populated ones still gate everywhere — verified on the knowledge-base anchor, which is a different code path from the credential anchor.
Untouched: mapping entries themselves (
mapping-service.tshardcodesrequired: true, and a blank credential never becomes an entry), nesteduser-onlyparams with a value,requiredComplete,syncDisabled, and the cleared-ref blocker list.Verification
Fork diff before → after, against a workflow built and deployed through the real editor:
projectIdissueKeytools[0].issueKeytools[0].issueKeysourceValue: ACME-999)documentSelectordocumentSelectorRow count unchanged (7 → 7). Push and pull directions produce identical flags.
End-to-end through the UI, on a forked workspace:
documentSelector = doc_child→ after sync the target block holdsdoc=doc_child.cred_jira_a → cred_jira_child, KBkb_parent → kb_child, blanks preserved.{"restored":1}with the undo consumed; unlink leftparent: null.Tests
apps/sim/lib/workflows/tool-input/param-visibility.test.ts(new),dependent-field-noun.test.ts(new), plus 12 cases added todependent-reconfigs.test.ts. Every behavioral case was verified to fail without the fix, not just pass with it.Full
apps/simsuite: 1,968 files / 26,289 tests passing.turbo run type-checkclean across all 24 workspaces.check:api-validationpasses.Review findings fixed in-branch
The pre-landing review caught a real defect in the first commit, fixed in
3ca8d29:isNonEmptyValueaboutrawSourceValue, which flattens every non-string to''for the wire contract. A multi-select dependent stores an array, so a populated one reported blank and stopped gating. Reachable today via zoho-deskdepartmentIds. Now asks the raw value.paramId..every()over an empty array (vacuously true) now pins both shapes.Select ${title.toLowerCase()}rendered "Select select issue" for titles that already read as instructions. Extracted and tested; a bare"Select"title now falls back instead of rendering"Select "/"No found".Security specialist: no findings.
Deliberately out of scope
human_in_the_loop. Itsnotificationsubblock is also atool-input, but its tools execute directly with no model, so auser-or-llmblank there genuinely is missing. This change stops fork sync gating on it. The gap already exists in the editor — the HITL tool row shows no asterisk and saves blank — so fork sync was only masking it, and only for forked workspaces. The fix is editor-side (give those params explicitvisibility: 'user-only', which re-gates them through this same rule). Separate PR.conditiongating (sibling in-flight PR). It shrinks the blast radius but does not fix Cause B —write/read-bulk/get_projectlegitimately show the selector. Independent; can land in either order.Note for affected users
The toggle-to-manual workaround persists
advancedinto those workflows, which changes which member serializes. Harmless while both fields are empty, but if anyone later picks a project in the basic selector it will be silently dropped. Not auto-migrated — those blocks want toggling back by hand.