Skip to content

fix(forks): stop sync demanding config the source never had - #6766

Merged
icecrasher321 merged 5 commits into
stagingfrom
fix/fork-sync-false-required-blockers
Aug 16, 2026
Merged

fix(forks): stop sync demanding config the source never had#6766
icecrasher321 merged 5 commits into
stagingfrom
fix/fork-sync-false-required-blockers

Conversation

@mzxchandra

Copy link
Copy Markdown
Contributor

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 write whose 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 required applied to a nested tool param

emitAnchoredDependents() runs over two structurally different things: a block's own subblocks, and the params of each tool inside a tool-input. The nested pass read the raw block config and stamped required: isSubBlockRequired(...).

Jira's issueKey subblock declares required: { field: 'operation', value: ['read', …] }, so a Get Issue tool evaluated operation === 'read'required: true.

But Sim already models this as ParameterVisibility: the tool param is issueKey: { required: true, visibility: 'user-or-llm' }. Every other consumer honors that — the tool-row editor strips required for anything not user-only, and createLLMToolSchema keeps 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 * and Jira Account * carry asterisks; Select Project and Select Issue do not.

Cause B — demanding configuration the source never had

projectId hangs 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 a selectorKey are emitted, so the basic selector gated while its advanced twin manualProjectId — identical required and dependsOn, no selectorKey — 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 writes canonicalModes.projectId = 'advanced', and the dormant-member guard skips the row.

Not the remapper. A Jira project isn't a remappable kind — project-selector maps to selector-resource, absent from REGISTRY_KIND_TO_FORK_KIND, so it never reaches unmappedRequired.

Neither fix subsumes the other

  • The emptiness fix alone leaves a populated user-or-llm param gating after a parent swap, because effectiveDependentValue blanks the stored value whenever the parent changed in-session.
  • The visibility rule alone never touches top-level subblocks.

How

One predicate at the single out.push site, expressing two independent invariants:

  1. A sync carries the source's configuration across; it never invents configuration the source never had. Applies to every dependent.
  2. Inside a tool-input, only a user-only param 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.ts and used by both the editor and the sync gate, so they can't drift. Visibility comes from getToolInputParamConfigs — 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 a user-or-llm || llm-only denylist: 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.ts hardcodes required: true, and a blank credential never becomes an entry), nested user-only params 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:

Block Field Before After
Jira Write projectId true false Cause B
Jira Read Pinned issueKey true true control — still gates
Agent Blank tools[0].issueKey true false Cause A+B
Agent Pinned tools[0].issueKey true false Cause A only (sourceValue: ACME-999)
KB Blank Doc documentSelector false KB anchor
KB Pinned Doc documentSelector true KB anchor control

Row count unchanged (7 → 7). Push and pull directions produce identical flags.

End-to-end through the UI, on a forked workspace:

  • Credential mapped → badge reads "Fully mapped", Sync enabled.
  • A populated required row emptied → Sync disabled, reason "Reconfigure all required fields first".
  • Re-picked a document → persisted as documentSelector = doc_child → after sync the target block holds doc=doc_child.
  • Two real syncs ran: credential remapped cred_jira_a → cred_jira_child, KB kb_parent → kb_child, blanks preserved.
  • Repeat sync idempotent (no duplicate workflows); rollback {"restored":1} with the undo consumed; unlink left parent: null.

Tests

apps/sim/lib/workflows/tool-input/param-visibility.test.ts (new), dependent-field-noun.test.ts (new), plus 12 cases added to dependent-reconfigs.test.ts. Every behavioral case was verified to fail without the fix, not just pass with it.

Full apps/sim suite: 1,968 files / 26,289 tests passing. turbo run type-check clean across all 24 workspaces. check:api-validation passes.

Review findings fixed in-branch

The pre-landing review caught a real defect in the first commit, fixed in 3ca8d29:

  • Silent un-gating on non-string values. The predicate asked isNonEmptyValue about rawSourceValue, 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-desk departmentIds. Now asks the raw value.
  • Canonical id is an alias — no longer clobbers a param owning that key as its own paramId.
  • A test asserting .every() over an empty array (vacuously true) now pins both shapes.
  • The indexer mock is reset per test, so the new cases aren't order-dependent.
  • Placeholder copy: 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. Its notification subblock is also a tool-input, but its tools execute directly with no model, so a user-or-llm blank 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 explicit visibility: 'user-only', which re-gates them through this same rule). Separate PR.
  • Jira condition gating (sibling in-flight PR). It shrinks the blast radius but does not fix Cause B — write / read-bulk / get_project legitimately show the selector. Independent; can land in either order.

Note for affected users

The toggle-to-manual workaround persists advanced into 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.

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".
… 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.
@vercel

vercel Bot commented Aug 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Aug 16, 2026 7:31am

Request Review

@cursor

cursor Bot commented Aug 16, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes fork sync blocking and post-sync redeploy gating across many selector dependents; behavior is heavily tested but affects a critical workspace-fork path.

Overview
Fork sync no longer treats every required selector as blocking Sync when the source left it blank or when a nested tool param is meant for the model at runtime.

Gating rules — Pre-sync dependent reconfig rows still appear for in-place re-picks, but required now combines two checks: only user-only tool params count as the user’s job (via shared param-visibility helpers aligned with the tool-row editor), and emptiness uses the raw stored value (isNonEmptyValue) so multi-select arrays don’t look blank after string coercion.

Post-sync promote — Cleared nested tool dependents use the same visibility map from getToolInputParamConfigs, so a user-or-llm param cleared after a parent swap won’t incorrectly skip redeploy.

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-apps

greptile-apps Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR fixes fork synchronization so blank source configuration and model-supplied tool parameters do not incorrectly block synchronization or redeployment.

  • Centralizes user-required tool-parameter resolution across the editor, pre-sync collector, and cleared-dependent promotion path.
  • Keeps blank source dependents visible for configuration while making them non-gating.
  • Improves dependent selector placeholder copy and adds regression coverage for visibility, canonical aliases, non-string values, and cleared parameters.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

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]
Loading

Reviews (2): Last reviewed commit: "style(forks): use TSDoc for the new test..." | Re-trigger Greptile

Comment thread apps/sim/lib/workflows/tool-input/param-visibility.ts
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.
@mzxchandra

Copy link
Copy Markdown
Contributor Author

@greptile

@mzxchandra

Copy link
Copy Markdown
Contributor Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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.

@icecrasher321
icecrasher321 merged commit ad83796 into staging Aug 16, 2026
30 checks passed
@mzxchandra
mzxchandra deleted the fix/fork-sync-false-required-blockers branch August 16, 2026 17:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants